Skip to main content
Urbicon UI

Menu

Action menu for invoking actions. Items dispatch onSelect callbacks; an item with checked displays a setting. For committing a value to a form, use Select.

Playground

Last action:
Tier Style variant
Item Size Style variant
Chevron Animation
Trigger Variant
Mint
<script lang="ts">
  import { Menu } from '@urbicon-ui/blocks';

  const items = [
  { label: 'Dashboard', onSelect: () => console.log('Dashboard') },
  { label: 'User Settings', onSelect: () => console.log('User Settings') },
  { label: 'Notifications', onSelect: () => console.log('Notifications') },
  { label: 'Billing', onSelect: () => console.log('Billing') },
  { label: 'Help', onSelect: () => console.log('Help') }
];
</script>

<Menu
  {items}
  intent="neutral"
  itemSize=""
  placeholder="Actions"
  size="md"
  variant="outlined"
/>

01 When to use Menu

Menu invokes actions. To pick a value that binds to a form, reach for Select or Combobox instead.

ComponentRoleReach for it when
Menu (this)menuThe items are verbs: Edit, Delete, Share, Export. Each runs its onSelect. An item given checked also displays a setting — Menu shows the state you supply but never stores a selection.
SelectlistboxThe user commits a value to a form. Single or multiple.
ComboboxlistboxA value from a long list, narrowed by type-ahead.

02 Examples

Each item is an object with a label and an onSelect that runs when it is activated (a bare string is shorthand for a label-only item). Add id, disabled, keepOpen for repeated picks, checked for a selectable setting, detail for a right-aligned readout, or children for a submenu, and a type: 'section' entry heads a group and owns every item up to the next header; { type: 'divider' } draws a rule between two runs. Build the menu from an items array, or declaratively with <MenuItem>, <MenuSection> and <MenuDivider> children — there a section takes the items it names as its own children. When the built-in icon-label-detail row is not enough, a customItem snippet takes over each row's inner content — render visible content only, since Menu supplies the surrounding button.

Basic actions

Each item runs its onSelect when activated, and the menu closes again unless the item sets keepOpen. A section wraps the rows it names, so screen readers announce them as its group — the items array expresses the same shape with { type: 'section' } and { type: 'divider' } entries.
Last action:
<script lang="ts">
  import { Menu, MenuDivider, MenuItem, MenuSection } from '@urbicon-ui/blocks';

  let lastAction = $state('');
</script>

<div class="flex items-center gap-4">
  <Menu placeholder="File">
    <MenuItem label="New file" onSelect={() => (lastAction = 'New file')} />
    <MenuItem label="Open recent" onSelect={() => (lastAction = 'Open recent')} />
    <MenuSection label="Workspace">
      <MenuItem label="Settings" onSelect={() => (lastAction = 'Settings')} />
      <MenuItem label="Extensions" onSelect={() => (lastAction = 'Extensions')} />
    </MenuSection>
    <MenuDivider />
    <MenuItem label="Close window" onSelect={() => (lastAction = 'Close window')} />
  </Menu>
  <span class="text-text-tertiary text-sm">Last action: <code>{lastAction}</code></span>
</div>

Icon-only trigger

customTrigger replaces the default button. It receives toggle and open. Wire toggle to onclick and open to aria-expanded. The usual shape is a compact icon button for row or card overflow actions.
Last action:
<script lang="ts">
  import { Button, Menu, type MenuObjectOption, MoreHorizontalIcon } from '@urbicon-ui/blocks';

  let lastAction = $state('');

  const items: MenuObjectOption[] = [
    { label: 'Rename', onSelect: () => (lastAction = 'Rename') },
    { label: 'Duplicate', onSelect: () => (lastAction = 'Duplicate') },
    { label: 'Delete', onSelect: () => (lastAction = 'Delete') }
  ];
</script>

<div class="flex items-center gap-4">
  <Menu {items}>
    {#snippet customTrigger(toggle, open)}
      <Button
        variant="ghost"
        size="sm"
        aria-label="More actions"
        aria-haspopup="menu"
        aria-expanded={open}
        onclick={toggle}
      >
        <MoreHorizontalIcon class="h-4 w-4" />
      </Button>
    {/snippet}
  </Menu>
  <span class="text-text-tertiary text-sm">Last action: <code>{lastAction}</code></span>
</div>

Account menu

customHeader and customFooter frame the item list with regions the array cannot express: a signed-in banner above, a destructive Sign out below.
Last action:
<script lang="ts">
  import { Menu, Button, type MenuObjectOption } from '@urbicon-ui/blocks';

  let lastAction = $state('');

  const items: MenuObjectOption[] = [
    { label: 'Profile', onSelect: () => (lastAction = 'Profile') },
    { label: 'Billing', onSelect: () => (lastAction = 'Billing') },
    { label: 'Team', onSelect: () => (lastAction = 'Team') }
  ];
</script>

<div class="flex items-center gap-4">
  <Menu placeholder="Account" {items}>
    {#snippet customHeader()}
      <div class="text-text-secondary text-xs font-medium">Logged in as jane@example.com</div>
    {/snippet}
    {#snippet customFooter()}
      <div class="flex justify-end">
        <Button variant="ghost" intent="danger" size="sm" onclick={() => (lastAction = 'Sign out')}>
          Sign out
        </Button>
      </div>
    {/snippet}
  </Menu>
  <span class="text-text-tertiary text-sm">Last action: <code>{lastAction}</code></span>
</div>

Selectable settings

checked turns a row into role=menuitemradio with a checkmark; the parent row's detail shows the current value while the submenu is collapsed. The state lives in the consumer — each onSelect updates it, Menu only displays it.
Sorted by Name
<script lang="ts">
  import { Menu, type MenuObjectOption } from '@urbicon-ui/blocks';

  let sortBy = $state('Name');

  // Menu displays the checked state but never stores it — `sortBy` here is
  // the single source of truth, updated by each item's onSelect.
  const sortOption = (label: string): MenuObjectOption => ({
    id: label.toLowerCase(),
    label,
    checked: sortBy === label,
    onSelect: () => (sortBy = label)
  });

  const items = $derived<MenuObjectOption[]>([
    {
      id: 'sort',
      label: 'Sort by',
      detail: sortBy,
      children: [sortOption('Name'), sortOption('Date'), sortOption('Size')]
    },
    { id: 'refresh', label: 'Refresh' }
  ]);
</script>

<div class="flex items-center gap-4">
  <Menu placeholder="View" {items} />
  <span class="text-text-tertiary text-sm">Sorted by <code>{sortBy}</code></span>
</div>

03 Customization

Primary-accented panel via slotClasses

slotClasses reaches any slot by name. Open the menu: the panel takes a primary-tinted border and a lifted shadow, and hovering an item tints it primary. Radius, spacing and dismiss behavior stay on the component.
<Menu
  placeholder="Actions"
  items={['Rename', 'Duplicate', 'Archive']}
  slotClasses={{
    content: 'border-primary/30 shadow-[var(--blocks-shadow-lg)]',
    item: 'hover:bg-primary/10 hover:text-primary'
  }}
/>

This is one of five ways to restyle a block. See Customization for class, slotClasses, unstyled, preset and provider-level overrides.

04 Accessibility

Built-in ARIA

Uses role="menu" on the panel and role="menuitem" on each item, with aria-haspopup="menu" + aria-expanded on the trigger. An item given checked renders as role="menuitemradio" with aria-checked, so the active setting is announced, not just marked. Sub-menus add aria-haspopup="menu" on the submenu trigger. A section renders its header as role="presentation" and wraps the items it names in a role="group" labelled by that header, so a radio set is announced with the group it belongs to; role="separator" is reserved for the divider.

Keyboard

Enter / Space on the trigger to open. Arrow keys move focus between items (roving tabindex), Home / End jump to the first/last item, and Tab moves focus out and closes the menu (W3C menu pattern). Enter / Space activates an item. Escape closes the menu and restores focus to the trigger.

Focus Management

On activation the menu closes and focus returns to the trigger. Items with keepOpen dispatch their action without closing. Useful for repeated actions like "Add tag".

05 API Reference

40 props
40 props
Prop
Type
Default
Description

06 Types

Local type definitions used by this component.

22 types
Name
Kind
Category
Used by
Description

07 Installation

Import

import { Menu, MenuItem, MenuDivider, MenuSection } from '@urbicon-ui/blocks';