Menu
Action menu for invoking actions. Items dispatch onSelect callbacks. For selecting a value from a list, use Select.
Playground
—<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
—<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
—<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)
—<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
——<!-- 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
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
Prop | Type | Default | Description | |
|---|---|---|---|---|
chevronAnimation inherited | rotatetranslatefadenone | — | chevronAnimation property | |
children inherited | Snippet | — | Declarative children mode — use <MenuItem> / <MenuDivider> / <MenuSection>. | |
class | string | — | Additional CSS classes to apply to the Menu component | |
contextTrigger inherited | Snippet | — | Turn the menu into a **context menu**: instead of a trigger button, the
snippet you pass becomes a right-click target. A contextmenu (right-click
or long-press) on it opens the menu at the cursor position — the native
browser context menu is suppressed. Keyboard navigation, dismissal and
item selection behave exactly as in the dropdown menu; on dismiss, focus
returns to wherever it was. Mutually exclusive with customTrigger/the
default trigger button (when set, no trigger button renders). | |
customFooter inherited | Snippet | — | Optional custom footer rendered below the items list. | |
customHeader inherited | Snippet | — | Optional custom header rendered above the items list. | |
customItem inherited | Snippet<[TItem]> | — | Custom per-item content. **Render visible content only** — the outer
role="menuitem" button is provided by Menu and handles the click /
keyboard activation. Putting an interactive element (<button>, <a>)
inside the snippet creates nested-interactive HTML and triggers the
item's action twice via event bubbling.
Positional arg: (item). To dispatch from outside the normal click
(e.g. a "Recent" entry that needs to activate from a parent shortcut),
call the item's own onSelect directly. | |
customTrigger inherited | Snippet<[() => void, boolean, () => void]> | — | Replace the default trigger button (chevron + label) with a custom element.
Positional args: (toggle, open, dismiss).
- toggle: flips the open state — wire this to your custom trigger's
click handler so the menu can be opened from the consumer's element.
- open: current open state — useful for aria-expanded.
- dismiss: closes the menu without changing toggle history; rarely
needed but provided for symmetry.
The consumer's element should bind its onclick to toggle and set
aria-expanded={open} + aria-haspopup="menu" for ARIA correctness. | |
disabled variant | true | — | Controls the disabled behavior and appearance of the Menu component. Available options: true. | |
getItemChildren inherited | (item: TItem) => TItem[] | undefined | — | getItemChildren property | |
getItemDisabled inherited | (item: TItem) => boolean | — | getItemDisabled property | |
getItemIcon inherited | (item: TItem) => unknown | — | Optional icon resolver for items in array mode. | |
getItemId inherited | (item: TItem) => string | — | GetItemId property for the Menu component | |
getItemLabel inherited | (item: TItem) => string | — | Optional mapping functions for custom item shapes. | |
getSectionLabel inherited | (item: MenuSectionHeader) => string | — | Section label override. Accepts concrete section header type. | |
id | string | — | Id property for the Menu component | |
intent inherited | ButtonVariants['intent'] | 'neutral' | Button intent applied to the default trigger button. | |
isSection inherited | (item: MenuItemType) => boolean | — | Section detection override. Applies to full union, not just TItem. | |
items inherited | TItem[] | — | Array of menu items. Each item's onSelect runs when activated. | |
itemSize variant | lgmdsm | md | Controls the itemSize behavior and appearance of the Menu component. Available options: lg, md, sm. | |
loading inherited | boolean | false | Whether the default trigger button is in loading state. | |
mint inherited | MintProp | 'none' | Micro-interaction preset forwarded to the inner default trigger Button and applied to each menu item row (per-item via context). Only applies while not disabled. | |
onOpenChange inherited | (open: boolean) => void | — | Fires when the menu opens or closes from user interaction (trigger
click, item activation, Escape, Tab-out, outside click). Receives the
new open state. Not called when the consumer writes bind:open directly. | |
open variant | true | false | Controls the open behavior and appearance of the Menu component. Available options: true. | |
placeholder inherited | string | — | Placeholder text shown on the default trigger. Acts as the trigger's
accessible name unless aria-label is supplied. Typical values: "Actions",
"More", "Options". Ignored when customTrigger is provided. | |
placement variant | bottombottom-endbottom-starttop +2 more | bottom-start | Controls the positioning and alignment of the Menu relative to its container or trigger element. Available options: bottom, bottom-end, bottom-start, and 3 more. | |
preset inherited | string | — | Apply a named preset registered via <BlocksProvider presets={{ Menu: {...} }}>.
Prefer this over class overrides when the requested look falls outside the
semantic intent palette — presets keep hover/active/dark-mode logic coherent
and make the custom look reusable across the project. | |
size inherited | ButtonVariants['size'] | 'md' | Button size applied to the default trigger button. | |
slotClasses inherited | Partial<Record<MenuSlots, string>> | — | Per-slot class overrides merged with tailwind-variants styles. Slots: base | trigger | triggerText | chevron | content | header | section | divider | items | item | indicator | submenu | footer | |
syncWidth variant | falsetrue | true | Controls the syncWidth behavior and appearance of the Menu component. Available options: false, true. | |
tier variant | commitmodify | commit | Selects the semantic radius tier of the Menu — the shape family it belongs to (--radius-commit/-modify/-contain/-bridge). Shape is retuned per family in your theme, so this picks the family rather than a pixel value. Available options: commit, modify. | |
unstyled inherited | boolean | false | Remove default tailwind-variants classes. Only user-supplied classes apply. | |
usePortal variant | falsetrue | true | Controls the usePortal behavior and appearance of the Menu component. Available options: false, true. | |
variant inherited | ButtonVariants['variant'] | 'outlined' | Button variant applied to the default trigger button. | |
...HTMLAttributes<HTMLDivElement> inherited | HTMLAttributes | — | HTML attributes (excluding: 'children' | 'class' | 'placeholder') | |
...MenuVariants variant | VariantProps | — | Styling variants from MenuVariants |
06 Types
Local type definitions used by this component.
Name | Kind | Category | Used by | Description | |
|---|---|---|---|---|---|
MenuSpecificProps | interface | props | 0 | Menu-specific props. The catalog JSDoc (@description/@tag/@related)
lives on MenuProps below — the interface docs-gen actually picks up —
so it is not duplicated here. | |
MenuProps | interface | props | 0 | — | |
MenuOption | type | helper | 0 | Shorthand item form: the string is the label and serves as the item's
stable id (for sub-menu bookkeeping). Use MenuObjectOption to attach
an onSelect callback or other rich item state. | |
MenuSectionHeader | interface | helper | 0 | Section header item for grouping related menu options. | |
MenuObjectOption | interface | helper | 0 | Menu item with explicit label, action callback, and optional nested
children. Menu items are *verbs* — the id is only an internal stable
identifier for sub-menu bookkeeping and DOM id derivation, not a
selectable value. (Menu has no selection state — for value-picking use
Select.) | |
MenuItemType | type | helper | 0 | Union of all supported menu item shapes. - string: simple item label (value equals label) - MenuObjectOption: rich item with label/onSelect and optional children - MenuSectionHeader: non-selectable header grouping the following items | |
MenuCustomSlots | interface | variant | 0 | — | |
MenuVariants | type | variant | 1 | — | |
MenuIconVariants | type | variant | 0 | — | |
MenuSlots | type | variant | 0 | Slot names derived from the tv() config — single source of truth for slotClasses. | |
MintProp | type | helper | 1 | — | |
ButtonVariants | type | variant | 0 | — | |
AnimationProps | interface | props | 0 | — | |
Placement | type | helper | 0 | — | |
InteractiveTier | type | helper | 0 | Semantic radius tier for interactive surfaces (3-tier system).
- commit → r-human (CTA, identity, status declarations)
- modify → r-interactive (fields, navigation, secondary actions)
Container components (Card, Alert, Toolbar surface, …) live in a third
tier contain (r-structure) which is **not** part of this propagation
context — those surfaces are always r-structure by design and have no
tier-flip use case. | |
MintName | type | helper | 0 | A mint name: a built-in (autocompleted), 'none' to disable, or any
consumer-registered name. (string & {}) keeps the registry open — a
custom name still type-checks, it just isn't suggested. A typo therefore
also still compiles (it resolves like an unregistered custom name and
warns at runtime); the union buys completion and docs, not validation. | |
MintConfig | interface | helper | 0 | — | |
VariantProps | type | helper | 1 | — | |
Side | type | helper | 0 | — | |
Alignment | type | helper | 0 | — | |
BuiltinMintName | type | helper | 0 | Built-in mint names as a literal union, so the mint prop autocompletes
across every component — the single list the hand-curated playground knobs
and docs used to drift away from. |
07 Installation
Import
import { Menu, MenuItem, MenuDivider, MenuSection } from '@urbicon-ui/blocks';