Skip to main content
Urbicon UI

Menu

Action menu for invoking actions. Items dispatch onSelect callbacks. For selecting a value from a list, use Select.

Playground

Last action:
Tier Style variant (tailwind-variants)
Item Size Style variant (tailwind-variants)
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 (vs. Select)

02 Examples

Basic actions

Items array — each item carries an onSelect callback. Sections group related actions; the menu closes after activation unless keepOpen is set on the item.
Last action:
<script lang="ts">
  import { Menu, type MenuItemType } from '@urbicon-ui/blocks';

  let lastAction = $state('');
  const items: MenuItemType[] = [
    { label: 'New file', onSelect: () => (lastAction = 'New file') },
    { label: 'Open recent', onSelect: () => (lastAction = 'Open recent') },
    { type: 'section', label: 'Workspace' },
    { label: 'Settings', onSelect: () => (lastAction = 'Settings') }
  ];
</script>

<div class="flex items-center gap-4">
  <Menu placeholder="File" {items} />
  <span class="text-text-tertiary text-sm">Last action: <code>{lastAction}</code></span>
</div>

Declarative children

Build the menu with MenuItem, MenuSection and MenuDivider instead of an items array. Pass onSelect on each MenuItem to wire the action.
Last action:
<script lang="ts">
  import { Menu, MenuItem, MenuSection, MenuDivider } from '@urbicon-ui/blocks';

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

<div class="flex items-center gap-4">
  <Menu placeholder="More">
    <MenuSection label="Main" />
    <MenuItem label="Dashboard" onSelect={() => (lastAction = 'Dashboard')} />
    <MenuItem label="Settings" onSelect={() => (lastAction = 'Settings')} />
    <MenuDivider />
    <MenuSection label="Other" />
    <MenuItem label="Help" disabled />
  </Menu>
  <span class="text-text-tertiary text-sm">Last action: <code>{lastAction}</code></span>
</div>

Custom trigger (icon button)

Replace the default trigger entirely via the customTrigger snippet. Receives dismiss + open — typical pattern is an icon-only button like MoreHorizontal.
Last action:
<script lang="ts">
  import { Button, Menu, type MenuObjectOption, getIcon } from '@urbicon-ui/blocks';

  const MoreHorizontalIcon = getIcon('moreHorizontal');

  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>

Custom snippets

customHeader, customFooter and customItem replace the regions an items array cannot express — a sign-in banner, a destructive footer action, a row with an avatar and a shortcut. customItem takes one positional argument, the item, and should render visible content only: Menu already provides the surrounding role='menuitem' button, so an interactive element inside the snippet nests one control in another and fires the action twice.
Last action:
Last action:
<!-- CustomHeaderFooter.svelte -->
<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>

<!-- CustomItemRenderer.svelte -->
<script lang="ts">
  import {
    ArchiveIcon,
    ArrowUpRightIcon,
    LinkIcon,
    Menu,
    type MenuObjectOption
  } from '@urbicon-ui/blocks';

  let lastAction = $state('');

  const items: MenuObjectOption[] = [
    { id: 'copy', label: 'Copy link', onSelect: () => (lastAction = 'Copy link') },
    { id: 'open', label: 'Open in new tab', onSelect: () => (lastAction = 'Open in new tab') },
    { id: 'archive', label: 'Archive', onSelect: () => (lastAction = 'Archive') }
  ];

  const icons = {
    copy: LinkIcon,
    open: ArrowUpRightIcon,
    archive: ArchiveIcon
  };
</script>

<div class="flex items-center gap-4">
  <Menu placeholder="Actions" {items}>
    {#snippet customItem(item)}
      <!--
        Render visible content only — Menu's outer button handles activation.
        Putting another <button> inside would nest interactive elements and
        fire the item's onSelect twice via event bubbling.
      -->
      {@const Icon = icons[(item as MenuObjectOption).id as keyof typeof icons]}
      <span class="flex w-full items-center gap-3">
        <Icon size={16} />
        <span class="flex-1 truncate">{(item as MenuObjectOption).label}</span>
      </span>
    {/snippet}
  </Menu>
  <span class="text-text-tertiary text-sm">Last action: <code>{lastAction}</code></span>
</div>

03 Customization

Soft panel via slotClasses

Menu exposes its full anatomy as slots — trigger, content, item, section, divider, indicator, submenu, and footer among them. Here the floating panel gets a softer radius and shadow while the items pick up a primary hover tint; placement, keyboard navigation, and dark mode stay untouched.
<Menu
  placeholder="Actions"
  items={['Rename', 'Duplicate', 'Archive']}
  slotClasses={{
    content: 'rounded-xl shadow-[var(--blocks-shadow-lg)]',
    item: 'rounded-lg hover:bg-primary/10 hover:text-primary'
  }}
/>

unstyled strips the default classes from every slot while keeping role="menu" semantics, roving focus, and dismiss behavior — rebuild the look entirely through slotClasses. A context-menu skin you repeat across the app belongs in a BlocksProvider preset (registered under presets.Menu, applied per instance via preset) — see Customization.

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. Sub-menus add aria-haspopup="menu" on the submenu trigger.

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

36 props
36 props
Prop
Type
Default
Description

06 Types

Local type definitions used by this component.

21 types
Name
Kind
Category
Used by
Description

07 Installation

Import

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