Navigation
Menu
MD3 menu — a list of actions hanging off a trigger.
import { Menu } from 'omaris' Examples
Basic
The trigger snippet gets the props that wire it to the menu. The overflow on a row or a card, where three buttons would crowd it.
<script lang="ts"> import { Button, Menu, MenuItem } from 'omaris';</script><Menu label="Project actions"> {#snippet trigger(props)} <Button variant="outlined" {...props}>Actions</Button> {/snippet} <MenuItem>Rename</MenuItem> <MenuItem>Duplicate</MenuItem> <MenuItem tone="destructive">Delete</MenuItem></Menu> Icons and shortcuts
Icons and shortcuts earn their place on a menu people open often; inset lines up a row without an icon beside the ones that have one.
<script lang="ts"> import { IconButton, Menu, MenuItem, MenuLabel } from 'omaris';</script><Menu label="Edit"> {#snippet trigger(props)} <IconButton aria-label="Edit menu" {...props}> <svg viewBox="0 0 24 24" fill="currentColor"> <circle cx="12" cy="5" r="1.6" /><circle cx="12" cy="12" r="1.6" /><circle cx="12" cy="19" r="1.6" /> </svg> </IconButton> {/snippet} <MenuLabel>Edit</MenuLabel> <MenuItem shortcut="⌘C"> {#snippet icon()} ... {/snippet} Copy </MenuItem> <MenuItem shortcut="⌘V"> {#snippet icon()} ... {/snippet} Paste </MenuItem> <MenuItem inset shortcut="⌘⌫" tone="destructive">Delete</MenuItem></Menu> Selection
selected draws the tick. selected={false} is meaningfully different from leaving it off: it reserves the space, so a list of choices does not shuffle sideways when one is picked. radio makes each row one of a set, which is what a screen reader announces.
<script lang="ts"> import { Button, Menu, MenuItem } from 'omaris'; let sort = $state('Newest first');</script><Menu label="Sort by" closeOnSelect> {#snippet trigger(props)} <Button variant="tonal" {...props}>Sort: {sort}</Button> {/snippet} <MenuItem radio selected={sort === 'Newest first'} onclick={() => (sort = 'Newest first')}> Newest first </MenuItem> <MenuItem radio selected={sort === 'Oldest first'} onclick={() => (sort = 'Oldest first')}> Oldest first </MenuItem> <MenuItem radio selected={sort === 'Largest total'} onclick={() => (sort = 'Largest total')}> Largest total </MenuItem></Menu> Placement
side and align place it, and it flips itself when there is no room. align="end" for a menu hanging off the far end of a toolbar.
<script lang="ts"> import { Button, Menu, MenuItem } from 'omaris';</script><Menu side="top" align="start" label="Upwards"> {#snippet trigger(props)} <Button variant="outlined" size="xs" {...props}>Top</Button> {/snippet} <MenuItem>Opens above</MenuItem></Menu><Menu side="right" align="start" label="Sideways"> {#snippet trigger(props)} <Button variant="outlined" size="xs" {...props}>Right</Button> {/snippet} <MenuItem>Opens beside</MenuItem></Menu><Menu matchWidth label="Matched"> {#snippet trigger(props)} <Button variant="outlined" size="xs" class="w-48 max-w-full" {...props}>matchWidth</Button> {/snippet} <MenuItem>As wide as the trigger</MenuItem></Menu> When to use it
Use it for
- Actions off a trigger: the overflow on a row or card behind an Icon Button, the account menu, "more" at the end of a toolbar.
- A single choice the trigger names, like "Density: comfortable".
selecteddraws the tick on the current row, andradiomakes the rows one set. - A second level that would crowd the first: "Move to ▸", "Sort by ▸" as a
MenuSub. On a phone it expands in place inside the sheet. - A set of toggles changed in one visit.
closeOnSelect={false}keeps it open;selected={false}reserves the tick's space so rows do not shift. - Links, when a row has
href.
Not for
- A value in a form → Select; one you search for → Combobox. A Menu does something; a Select holds something.
- The main action and its alternatives → Split Button.
- Moving between the app's sections → Navigation Drawer.
- Anything with a field in it, or a decision to confirm → Dialog or Sheet.
- Explaining what a control does → Tooltip.
Do
- Spread the trigger snippet's props onto the real button, so the ARIA and the click land on the element that has focus.
- Put the destructive row last, after a Divider, with
tone="destructive". - Give every row an
iconor none.insetlines up the odd one out. - Use
shortcutonly for a shortcut that works on the page.
Don't
- Nest one menu in another; there is no submenu. Group with
MenuLabeland a Divider, or move the set into a Sheet. - Set
selectedon a row that is an action. A tick means a state, and "Delete ✓" makes no sense. - Build a row without
data-menu-item. The arrow keys find rows by it;MenuItemsets it, your own<button>does not. - Let it grow past a dozen rows. Past that it scrolls, and a scrolled menu should be a page.
Quick reference
size Menusmmd(default)lg
tone MenuItemdefault(default)destructive
API
Menu
MD3 menu — a list of actions hanging off a trigger.
The trigger snippet is handed the ARIA and the click handler to spread onto whatever opens the menu, so the attributes land on the real button rather than a wrapper. The surface is portalled and anchored, so it escapes scroll containers and flips when it runs out of room.
Keyboard: Enter/Space/↓ open it, ↑↓ move, Home/End jump, typing jumps to a matching item, Escape closes and hands focus back to the trigger.
import { Menu } from 'omaris' Parts
Every element this renders is reachable from outside. Pass Tailwind for one part as classes={{ part: '…' }}; class covers the root and is merged last, so it always wins.
surface- No description in the source yet.
trigger- The wrapper around whatever opens the menu —
w-full, usually.
Props
open bindableDefaults to false
boolean Bindable.
side Defaults to 'bottom'
AnchorSide align Defaults to 'start'
AnchorAlign offset Defaults to 6
number size Defaults to 'md'
MenuSize smmdlg
matchWidth Defaults to false
boolean Make the menu at least as wide as its trigger.
closeOnSelect Defaults to true
boolean Leave the menu open after an item is chosen — for a set of toggles.
label string Accessible name for the menu.
mobileSheet Defaults to true
boolean On a phone, rise from the bottom edge as a sheet — full width, a scrim behind, dragged down to dismiss, rows a thumb can hit — instead of hanging off the trigger. false keeps it anchored.
class string classes MenuClasses Per-part Tailwind overrides. class still covers the root.
trigger Snippet<[MenuTriggerProps]> Receives the props to spread onto the opening control.
children Snippet The items — MenuItem, MenuLabel, Divider. Arrow-key navigation finds rows by their data-menu-item attribute, which MenuItem sets for you; an item you build yourself needs that attribute or the keyboard will skip it.
aria-haspopup required 'menu' aria-expanded required boolean aria-controls required string | undefined onclick required (event: MouseEvent) => void onkeydown required (event: KeyboardEvent) => void MenuItem
A row in a Menu.
Renders as a link when given href, otherwise a button. selected gives you the checkable variant (MD3's menu with a leading tick); tone="destructive" is the delete row.
import { MenuItem } from 'omaris' Parts
Every element this renders is reachable from outside. Pass Tailwind for one part as classes={{ part: '…' }}; class covers the root and is merged last, so it always wins.
root- No description in the source yet.
label- The label. Truncates by default; override to let it wrap.
shortcut- The right-aligned
shortcuthint. check- The tick drawn while
selected.
Props
tone Defaults to 'default'
MenuItemTone defaultdestructive
selected boolean Checkable row: shows a trailing tick when true. Leaving it undefined keeps the row a plain menuitem rather than a checkbox.
radio Defaults to false
boolean One of a set, rather than a toggle of its own: the checkable row becomes a menuitemradio, which is what a screen reader needs to say "Sort by: Name, selected, 1 of 3".
inset Defaults to false
boolean Indent to line up with rows that have a leading icon.
shortcut string Right-aligned hint — a keyboard shortcut, a count.
ripple Defaults to true
boolean class string classes MenuItemClasses Per-part Tailwind overrides. class still covers the root.
icon Snippet Leading icon.
end Snippet Trailing content, before the tick.
children Snippet variant ButtonVariant Fill style. MD3's five, plus link for inline navigation.
size ButtonSize MD3 Expressive heights: 32 / 40 / 56 / 96 / 136dp.
shape ButtonShape Pill (round) or MD3 rounded rectangle (square).
confetti boolean | ConfettiOptions Confetti on press. true for the default burst, or any ConfettiOptions — confetti={{ preset: 'fireworks' }}.
loading boolean Grows a spinner in at the start of the button and blocks interaction. The label stays put; only an icon button swaps its icon out.
toggle boolean Turns the button into a two-state toggle driven by pressed. Ignored when href is set — a link has no pressed state.
pressed bindableboolean Selected state of a toggle button. Bindable.
MenuLabel
A heading over a run of menu items.
Presentational only — it is never in the focus order and screen readers skip past it to the items themselves.
import { MenuLabel } from 'omaris' Props
class string children Snippet MenuSub
A row that opens a menu of its own — "Move to ▸", "Sort by ▸".
On a desktop the submenu hangs off the row's far edge: it opens on hover after a short intent delay, on a press, or on → (← in a right-to-left document), and ← or Escape comes back out to the row.
On a phone the parent menu is a bottom sheet, and a second sheet on top of it — or a panel hanging off the side of a full-width one — would be worse than no submenu at all. There the row expands in place instead, its items indented under it, the way a native settings list does.
import { MenuSub } from 'omaris' Parts
Every element this renders is reachable from outside. Pass Tailwind for one part as classes={{ part: '…' }}; class covers the root and is merged last, so it always wins.
trigger- The row that opens it. Styled by
MenuItem; this adds the chevron's room. chevron- No description in the source yet.
surface- The submenu's panel, when it hangs off the side.
inline- The in-place list, when the menu is a sheet.
Props
label required string The row's text.
open bindableDefaults to false
boolean Bindable.
delay Defaults to 120
number Delay before a hover opens it, in ms — long enough to cross it on the way down.
class string classes MenuSubClasses icon Snippet Leading icon on the row.
children Snippet The submenu's rows — MenuItem, MenuLabel, Divider, another MenuSub.