# Menu

Use `ui-menu` for compact action menus, overflow controls, contextual actions, and split-button patterns. Use `ui-menu-list` when you need to render the list surface itself, for example in custom overlays or embedded command panels.

## Import
```ts
import { MenuComponent, MenuListComponent, type MenuItem, type MenuSection } from 'ui';
```

## Basic menu button
```ts
import { Component, signal } from '@angular/core';
import { ButtonComponent, MenuComponent, type MenuItem } from 'ui';

@Component({
  selector: 'app-menu-basic-demo',
  standalone: true,
  imports: [ButtonComponent, MenuComponent],
  template: `
    <div style="display:flex;flex-direction:column;gap:1rem;width:100%;max-width:38rem">
      <div
        style="display:flex;flex-wrap:wrap;gap:0.75rem;align-items:center;padding:0.75rem 0.875rem;border:1px dashed var(--color-neutral-stroke-rest);border-radius:1rem;background:var(--color-neutral-background2-rest)"
      >
        <ui-button variant="secondary" appearance="outline" (click)="reset()">Reset</ui-button>
        <span style="font-size:0.8125rem;color:var(--color-neutral-foreground2-rest)">
          Last action: <strong>{{ lastAction() || 'none' }}</strong>
        </span>
      </div>

      <div
        style="display:flex;flex-wrap:wrap;gap:1rem;align-items:center;padding:1rem;border:1px solid var(--color-neutral-stroke-rest);border-radius:1rem;background:var(--color-neutral-background-rest)"
      >
        <ui-menu
          triggerVariant="dropdown"
          text="View"
          icon="apps"
          [menuItems]="viewItems"
          appearance="filled"
          variant="primary"
          (menuItemClick)="onItemClick($event)"
        />

        <ui-menu
          triggerVariant="button"
          text="Open quick panel"
          icon="open"
          [menuItems]="[]"
          appearance="outline"
          variant="secondary"
          (primaryClick)="lastAction.set('Quick panel opened')"
        />
      </div>
    </div>
  `,
})
export class MenuBasicDemoComponent {
  protected readonly lastAction = signal('');

  protected readonly viewItems: MenuItem[] = [
    { id: 'board', label: 'Board', icon: 'board' },
    { id: 'calendar', label: 'Calendar', icon: 'calendar' },
    { id: 'timeline', label: 'Timeline', icon: 'clock' },
  ];

  protected onItemClick(item: MenuItem): void {
    this.lastAction.set(item.label);
  }

  protected reset(): void {
    this.lastAction.set('');
  }
}
```

## Dropdown, split, and button triggers
```ts
import { Component, signal } from '@angular/core';
import { ButtonComponent, MenuComponent, type MenuItem } from 'ui';

@Component({
  selector: 'app-menu-trigger-variants-demo',
  standalone: true,
  imports: [ButtonComponent, MenuComponent],
  template: `
    <div style="display:flex;flex-direction:column;gap:1rem;width:100%;max-width:48rem">
      <div
        style="display:flex;flex-wrap:wrap;gap:0.75rem;align-items:center;padding:0.75rem 0.875rem;border:1px dashed var(--color-neutral-stroke-rest);border-radius:1rem;background:var(--color-neutral-background2-rest)"
      >
        <ui-button variant="secondary" appearance="outline" (click)="reset()">Reset</ui-button>
        <span style="font-size:0.8125rem;color:var(--color-neutral-foreground2-rest)">
          Last action: <strong>{{ lastAction() || 'none' }}</strong>
        </span>
      </div>

      <div
        style="display:grid;grid-template-columns:repeat(auto-fit,minmax(14rem,1fr));gap:1rem;width:100%"
      >
        <div
          style="display:flex;flex-direction:column;gap:0.75rem;padding:1rem;border:1px solid var(--color-neutral-stroke-rest);border-radius:1rem;background:var(--color-neutral-background-rest)"
        >
          <div style="font-size:0.875rem;font-weight:600">Dropdown trigger</div>
          <div style="font-size:0.8125rem;color:var(--color-neutral-foreground2-rest)">
            A standard menu button with a dedicated chevron.
          </div>
          <ui-menu
            triggerVariant="dropdown"
            text="Share"
            icon="share"
            [menuItems]="shareItems"
            (menuItemClick)="onItemClick($event)"
          />
        </div>

        <div
          style="display:flex;flex-direction:column;gap:0.75rem;padding:1rem;border:1px solid var(--color-neutral-stroke-rest);border-radius:1rem;background:var(--color-neutral-background-rest)"
        >
          <div style="font-size:0.875rem;font-weight:600">Split trigger</div>
          <div style="font-size:0.8125rem;color:var(--color-neutral-foreground2-rest)">
            One primary action plus a compact menu for alternatives.
          </div>
          <ui-menu
            triggerVariant="split"
            text="Save"
            icon="save"
            [menuItems]="saveItems"
            variant="primary"
            (primaryClick)="lastAction.set('Saved current draft')"
            (menuItemClick)="onItemClick($event)"
          />
        </div>

        <div
          style="display:flex;flex-direction:column;gap:0.75rem;padding:1rem;border:1px solid var(--color-neutral-stroke-rest);border-radius:1rem;background:var(--color-neutral-background-rest)"
        >
          <div style="font-size:0.875rem;font-weight:600">Button trigger</div>
          <div style="font-size:0.8125rem;color:var(--color-neutral-foreground2-rest)">
            Acts like a plain button when no menu items are passed.
          </div>
          <ui-menu
            triggerVariant="button"
            text="Open details"
            icon="open"
            [menuItems]="[]"
            appearance="outline"
            (primaryClick)="lastAction.set('Opened details panel')"
          />
        </div>
      </div>
    </div>
  `,
})
export class MenuTriggerVariantsDemoComponent {
  protected readonly lastAction = signal('');

  protected readonly shareItems: MenuItem[] = [
    { id: 'copy-link', label: 'Copy link', icon: 'link' },
    { id: 'invite', label: 'Invite people', icon: 'person_add' },
    { id: 'export', label: 'Export PDF', icon: 'arrow_download' },
  ];

  protected readonly saveItems: MenuItem[] = [
    { id: 'save', label: 'Save', icon: 'save' },
    { id: 'save-as', label: 'Save as copy', icon: 'document_copy' },
    { id: 'save-template', label: 'Save as template', icon: 'document_add' },
  ];

  protected onItemClick(item: MenuItem): void {
    this.lastAction.set(item.label);
  }

  protected reset(): void {
    this.lastAction.set('');
  }
}
```

## Sections and shortcuts
```ts
import { Component, signal } from '@angular/core';
import { ButtonComponent, MenuListComponent, type MenuItem, type MenuSection } from 'ui';

@Component({
  selector: 'app-menu-sections-shortcuts-demo',
  standalone: true,
  imports: [ButtonComponent, MenuListComponent],
  template: `
    <div style="display:flex;flex-direction:column;gap:1rem;width:100%;max-width:42rem">
      <div
        style="display:flex;flex-wrap:wrap;gap:0.75rem;align-items:center;padding:0.75rem 0.875rem;border:1px dashed var(--color-neutral-stroke-rest);border-radius:1rem;background:var(--color-neutral-background2-rest)"
      >
        <ui-button variant="secondary" appearance="outline" (click)="reset()">Reset</ui-button>
        <span style="font-size:0.8125rem;color:var(--color-neutral-foreground2-rest)">
          Last action: <strong>{{ lastAction() || 'none' }}</strong>
        </span>
      </div>

      <div
        style="width:100%;max-width:20rem;padding:0.5rem;border:1px solid var(--color-neutral-stroke-rest);border-radius:1rem;background:var(--color-neutral-background-rest)"
      >
        <ui-menu-list
          [sections]="sections"
          size="medium"
          variant="primary"
          appearance="subtle"
          (itemClick)="onItemClick($event)"
          (submenuClick)="lastAction.set('Opened submenu for ' + $event.label)"
        />
      </div>
    </div>
  `,
})
export class MenuSectionsShortcutsDemoComponent {
  protected readonly lastAction = signal('');

  protected readonly sections: MenuSection[] = [
    {
      header: 'File',
      divider: true,
      items: [
        { id: 'new', label: 'New file', icon: 'document', shortcut: 'Ctrl+N' },
        { id: 'open', label: 'Open', icon: 'folder', shortcut: 'Ctrl+O' },
        { id: 'save', label: 'Save', icon: 'save', shortcut: 'Ctrl+S' },
      ],
    },
    {
      header: 'Edit',
      divider: true,
      items: [
        { id: 'undo', label: 'Undo', icon: 'arrow_undo', shortcut: 'Ctrl+Z' },
        { id: 'redo', label: 'Redo', icon: 'arrow_redo', shortcut: 'Ctrl+Shift+Z', disabled: true },
        { id: 'find', label: 'Find', icon: 'search', shortcut: 'Ctrl+F' },
      ],
    },
    {
      header: 'View',
      items: [
        { id: 'outline', label: 'Outline', icon: 'panel_left', selected: true },
        { id: 'comments', label: 'Comments', icon: 'comment' },
      ],
    },
  ];

  protected onItemClick(item: MenuItem): void {
    this.lastAction.set(item.label);
  }

  protected reset(): void {
    this.lastAction.set('');
  }
}
```

## Nested submenu flows
```ts
import { Component, signal } from '@angular/core';
import { ButtonComponent, MenuComponent, type MenuItem } from 'ui';

@Component({
  selector: 'app-menu-submenu-demo',
  standalone: true,
  imports: [ButtonComponent, MenuComponent],
  template: `
    <div style="display:flex;flex-direction:column;gap:1rem;width:100%;max-width:42rem">
      <div
        style="display:flex;flex-wrap:wrap;gap:0.75rem;align-items:center;padding:0.75rem 0.875rem;border:1px dashed var(--color-neutral-stroke-rest);border-radius:1rem;background:var(--color-neutral-background2-rest)"
      >
        <ui-button variant="secondary" appearance="outline" (click)="reset()">Reset</ui-button>
        <span style="font-size:0.8125rem;color:var(--color-neutral-foreground2-rest)">
          Last action: <strong>{{ lastAction() || 'none' }}</strong>
        </span>
      </div>

      <div
        style="display:flex;flex-wrap:wrap;gap:1rem;align-items:center;padding:1rem;border:1px solid var(--color-neutral-stroke-rest);border-radius:1rem;background:var(--color-neutral-background-rest)"
      >
        <ui-menu
          triggerVariant="dropdown"
          text="Insert"
          icon="document_add"
          [menuItems]="insertItems"
          appearance="outline"
          variant="secondary"
          (menuOpened)="lastAction.set('Opened insert menu')"
          (menuItemClick)="onItemClick($event)"
        />

        <ui-menu
          triggerVariant="dropdown"
          text="Export"
          icon="arrow_download"
          [menuItems]="exportItems"
          variant="primary"
          (menuOpened)="lastAction.set('Opened export menu')"
          (menuItemClick)="onItemClick($event)"
        />
      </div>
    </div>
  `,
})
export class MenuSubmenuDemoComponent {
  protected readonly lastAction = signal('');

  protected readonly insertItems: MenuItem[] = [
    {
      id: 'heading',
      label: 'Heading',
      icon: 'text_font',
      submenuItems: [
        { id: 'h1', label: 'Heading 1', icon: 'text_font' },
        { id: 'h2', label: 'Heading 2', icon: 'text_font' },
        { id: 'h3', label: 'Heading 3', icon: 'text_font' },
      ],
    },
    {
      id: 'media',
      label: 'Media',
      icon: 'image',
      submenuItems: [
        { id: 'image', label: 'Image', icon: 'image' },
        { id: 'carousel', label: 'Carousel', icon: 'arrow_circle_right' },
      ],
    },
    { id: 'divider', label: 'Divider', icon: 'divider_tall' },
  ];

  protected readonly exportItems: MenuItem[] = [
    {
      id: 'share',
      label: 'Share',
      icon: 'share',
      submenuItems: [
        { id: 'copy-link', label: 'Copy link', icon: 'link' },
        { id: 'invite-reviewers', label: 'Invite reviewers', icon: 'person_add' },
      ],
    },
    { id: 'pdf', label: 'PDF', icon: 'document' },
    { id: 'markdown', label: 'Markdown', icon: 'document' },
  ];

  protected onItemClick(item: MenuItem): void {
    this.lastAction.set(item.label);
  }

  protected reset(): void {
    this.lastAction.set('');
  }
}
```

## Contextual row actions
```ts
import { Component, signal } from '@angular/core';
import { ButtonComponent, MenuComponent, type MenuItem } from 'ui';

@Component({
  selector: 'app-menu-context-actions-demo',
  standalone: true,
  imports: [ButtonComponent, MenuComponent],
  template: `
    <div style="display:flex;flex-direction:column;gap:1rem;width:100%;max-width:46rem">
      <div
        style="display:flex;flex-wrap:wrap;gap:0.75rem;align-items:center;padding:0.75rem 0.875rem;border:1px dashed var(--color-neutral-stroke-rest);border-radius:1rem;background:var(--color-neutral-background2-rest)"
      >
        <ui-button variant="secondary" appearance="outline" (click)="reset()">Reset</ui-button>
        <span style="font-size:0.8125rem;color:var(--color-neutral-foreground2-rest)">
          Last action: <strong>{{ lastAction() || 'none' }}</strong>
        </span>
      </div>

      <div
        style="display:flex;flex-direction:column;gap:0.75rem;padding:1rem;border:1px solid var(--color-neutral-stroke-rest);border-radius:1rem;background:var(--color-neutral-background-rest)"
      >
        @for (item of rows; track item.id) {
          <div
            style="display:flex;align-items:center;justify-content:space-between;gap:1rem;padding:0.75rem 0.875rem;border:1px solid var(--color-neutral-stroke-rest);border-radius:0.875rem;background:var(--color-neutral-background-rest)"
          >
            <div style="display:flex;flex-direction:column;gap:0.1875rem;min-width:0">
              <div
                style="font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis"
              >
                {{ item.label }}
              </div>
              <div style="font-size:0.8125rem;color:var(--color-neutral-foreground2-rest)">
                {{ item.meta }}
              </div>
            </div>
            <ui-menu
              triggerVariant="dropdown"
              [menuItems]="item.menuItems"
              icon="more_horizontal"
              size="small"
              [ariaLabel]="'Row actions'"
              (menuItemClick)="onMenuAction($event, item.label)"
            />
          </div>
        }
      </div>
    </div>
  `,
})
export class MenuContextActionsDemoComponent {
  protected readonly lastAction = signal('');

  protected readonly rows = [
    {
      id: 'brief',
      label: 'Creative brief',
      meta: 'Updated 10 minutes ago',
      menuItems: [
        { id: 'open-brief', label: 'Open', icon: 'open' },
        { id: 'duplicate-brief', label: 'Duplicate', icon: 'document_copy' },
        { id: 'archive-brief', label: 'Archive', icon: 'archive' },
      ] as MenuItem[],
    },
    {
      id: 'assets',
      label: 'Campaign assets',
      meta: '5 files waiting for approval',
      menuItems: [
        { id: 'open-assets', label: 'Open', icon: 'open' },
        { id: 'share-assets', label: 'Share', icon: 'share' },
        { id: 'delete-assets', label: 'Delete', icon: 'delete', disabled: true },
      ] as MenuItem[],
    },
  ];

  protected onMenuAction(item: MenuItem, label: string): void {
    this.lastAction.set(`${item.label} on ${label}`);
  }

  protected reset(): void {
    this.lastAction.set('');
  }
}
```

## Overlay behavior in constrained shells
```ts
import { Component, signal } from '@angular/core';
import { ButtonComponent, MenuComponent, type MenuItem } from 'ui';

@Component({
  selector: 'app-menu-overflow-demo',
  standalone: true,
  imports: [ButtonComponent, MenuComponent],
  template: `
    <div style="display:flex;flex-direction:column;gap:1rem;width:100%;max-width:40rem">
      <div
        style="display:flex;flex-wrap:wrap;gap:0.75rem;align-items:center;padding:0.75rem 0.875rem;border:1px dashed var(--color-neutral-stroke-rest);border-radius:1rem;background:var(--color-neutral-background2-rest)"
      >
        <ui-button variant="secondary" appearance="outline" (click)="reset()">Reset</ui-button>
        <span style="font-size:0.8125rem;color:var(--color-neutral-foreground2-rest)">
          Last action: <strong>{{ lastAction() || 'none' }}</strong>
        </span>
      </div>

      <div style="font-size:0.875rem;line-height:1.5;color:var(--color-neutral-foreground2-rest)">
        The trigger sits inside a clipped shell, but the overlay should still escape and stay
        usable.
      </div>

      <div
        style="position:relative;height:8rem;overflow:hidden;padding:1rem;border:1px dashed var(--color-neutral-stroke-rest);border-radius:1rem;background:linear-gradient(180deg,var(--color-neutral-background2-rest),var(--color-neutral-background-rest))"
      >
        <div style="position:absolute;right:1rem;bottom:1rem">
          <ui-menu
            triggerVariant="dropdown"
            text="More"
            icon="more_horizontal"
            [menuItems]="items"
            (menuItemClick)="onItemClick($event)"
          />
        </div>
      </div>
    </div>
  `,
})
export class MenuOverflowDemoComponent {
  protected readonly lastAction = signal('');

  protected readonly items: MenuItem[] = [
    { id: 'duplicate', label: 'Duplicate', icon: 'document_copy' },
    { id: 'move', label: 'Move to folder', icon: 'folder' },
    { id: 'delete', label: 'Delete', icon: 'delete' },
  ];

  protected onItemClick(item: MenuItem): void {
    this.lastAction.set(item.label);
  }

  protected reset(): void {
    this.lastAction.set('');
  }
}
```

## Workspace command header
```ts
import { Component, signal } from '@angular/core';
import { ButtonComponent, MenuComponent, SearchComponent, type MenuItem } from 'ui';

@Component({
  selector: 'app-menu-workspace-header-demo',
  standalone: true,
  imports: [ButtonComponent, MenuComponent, SearchComponent],
  template: `
    <div
      style="display:flex;flex-direction:column;gap:1rem;width:100%;max-width:56rem;padding:1rem;border:1px solid var(--color-neutral-stroke-rest);border-radius:1rem;background:var(--color-neutral-background-rest)"
    >
      <div
        style="display:flex;flex-wrap:wrap;align-items:flex-start;justify-content:space-between;gap:1rem"
      >
        <div style="display:flex;flex-direction:column;gap:0.25rem">
          <div style="font-size:1rem;font-weight:600">Q3 launch workspace</div>
          <div style="font-size:0.875rem;color:var(--color-neutral-foreground2-rest)">
            Menus often live beside search, primary actions, and compact overflow actions in one
            command header.
          </div>
        </div>

        <div style="display:flex;flex-wrap:wrap;gap:0.75rem;align-items:center">
          <ui-search placeholder="Search tasks" style="width:14rem" />
          <ui-menu
            triggerVariant="split"
            text="Publish"
            icon="arrow_upload"
            [menuItems]="publishItems"
            variant="primary"
            (primaryClick)="lastAction.set('Published current version')"
            (menuItemClick)="onItemClick($event)"
          />
          <ui-menu
            triggerVariant="dropdown"
            [menuItems]="overflowItems"
            icon="more_horizontal"
            [ariaLabel]="'More actions'"
            (menuItemClick)="onItemClick($event)"
          />
        </div>
      </div>

      <div
        style="display:flex;flex-wrap:wrap;gap:0.75rem;align-items:center;padding:0.75rem 0.875rem;border:1px dashed var(--color-neutral-stroke-rest);border-radius:1rem;background:var(--color-neutral-background2-rest)"
      >
        <ui-button variant="secondary" appearance="outline" (click)="reset()">Reset</ui-button>
        <span style="font-size:0.8125rem;color:var(--color-neutral-foreground2-rest)">
          Last action: <strong>{{ lastAction() || 'none' }}</strong>
        </span>
      </div>
    </div>
  `,
})
export class MenuWorkspaceHeaderDemoComponent {
  protected readonly lastAction = signal('');

  protected readonly publishItems: MenuItem[] = [
    { id: 'publish-now', label: 'Publish now', icon: 'arrow_upload' },
    { id: 'schedule', label: 'Schedule publish', icon: 'calendar' },
    { id: 'save-draft', label: 'Save as draft', icon: 'save' },
  ];

  protected readonly overflowItems: MenuItem[] = [
    { id: 'rename', label: 'Rename workspace', icon: 'rename' },
    { id: 'permissions', label: 'Permissions', icon: 'shield' },
    { id: 'archive', label: 'Archive workspace', icon: 'archive' },
  ];

  protected onItemClick(item: MenuItem): void {
    this.lastAction.set(item.label);
  }

  protected reset(): void {
    this.lastAction.set('');
  }
}
```

## Accessibility

### Trigger and menu semantics
`ui-menu` exposes a button-like trigger. When it opens a menu, the trigger reflects `aria-haspopup="menu"` and `aria-expanded`. `ui-menu-list` renders the actual menu surface with `role="menu"`.

### Keyboard behavior
Keyboard handling matters most inside `ui-menu-list`.

| Key | Action |
| --- | --- |
| ArrowDown / ArrowUp | Move focus between enabled menu rows. |
| Enter / Space | Activate the focused row. |
| ArrowRight | Open a submenu when the focused row has nested items. |
| ArrowLeft | Close the current submenu level. |
| Escape | Close the current menu surface. |
| Tab | Closes the menu instead of tabbing through every row. |

### Labels and split actions
Icon-only triggers need a clear `ariaLabel`. For split buttons, the primary action and the dropdown affordance serve different purposes, so both need understandable labeling and should not duplicate hidden meaning.
