Menu
A trigger and the MenuList of commands it opens. It isn't modal: no focus trap, no scroll lock, nothing laid over the page, so the pointer passes over it and the page stays usable around it.
Demos
For usage examples, visit the component demo page: Menu
Import
import { Menu } from '@soroush.tech/design-system/Menu'Props
| Prop | Type | Default | Description |
|---|---|---|---|
trigger | ReactElement | - | The element that opens the menu - a Button, a Pressable. It is given aria-haspopup, aria-expanded and aria-controls; its own onClick / onKeyDown still run. Required. |
children | ReactNode | - | MenuItem rows and MenuDividers. |
isOpen | boolean | - | Controlled open state. When set, the menu only asks to open or close, through onOpen / onClose. |
defaultOpen | boolean | false | Uncontrolled initial open state. |
onOpen | () => void | - | Fired when the menu asks to open. |
onClose | (event, reason: MenuCloseReason) => void | - | Fired when the menu asks to close: 'escapeKey', 'select' (a row chosen), 'triggerClick' or 'blur'. |
onStep | (step: -1 | 1) => void | - | Fired when Left or Right has nowhere further to go inside the menu, so a menu bar can step to the next menu. Swapped right to left. |
onSelect | (value: string | number) => void | - | Fired with a row's value when a row that has one is chosen. |
align | 'start' | 'end' | 'start' | Which edge of the trigger the list lines up with. Mirrored right to left. |
marginThreshold | keyof Theme['space'] | 2 | The least room between the list and the window's edge. |
elevation | PaperElevation (0-24) | 8 | Shadow depth of the surface. |
borderRadius | keyof Theme['radii'] | theme.defaults.surfaceRadius, else square | Corner radius of the list. |
layer | keyof Theme['zOrder'] | 'modal' | Stacking layer - modal keeps a menu opened in a dialog above it. |
emptyLabel | ReactNode | - | Shown as one disabled row when the menu has no rows ("Coming soon"); without it, the list stays empty. |
color | keyof Theme['palette'] | - | Accent color for every row. |
textColor | keyof Theme['text'] | - | Base text color for every row. |
size | keyof Theme['sizes'] | - | Density token for every row. |
dense | boolean | - | Compact vertical padding for every row. |
aria-label | string | trigger's name | Names the list. Without it the trigger names it (aria-labelledby). |
className | string | - | Class on the menu's root. |
slotProps | { list? } | - | Props for a part: list is the role="menu" list - and every submenu's - for the app's data-*, class and aria-*; an onKeyDown runs before the list's own. Its role, id and name are the menu's. |
Notes
- Placement. Under the trigger, lined up with its start or end edge; above it when there's
no room below; moved only as far as it must to stay
marginThresholdinside the window. It follows the trigger on scroll and resize, and is hidden until first placed so it never shows at the wrong spot. It can't grow taller than the window. - Not portaled, not
Popover.Popoveris a modal - it moves focus in, traps it and hides the rest of the page from assistive technology, which suits a dialog and not a menu. The list is fixed in the window instead, inside the menu's box, so closing when focus leaves works and rows are found by their nearestrole="menu". An ancestor with atransform,filterorcontain- a zoom wrapper, a sliding drawer - makes itself the containing block ofposition: fixed; the list measures where it landed and moves back by the difference, so it still opens against its trigger. A scaling transform still scales it. - Submenus. A
MenuItemwithsubmenuopens a nested list beside it, placed and drawn the way the menu's own list is - seeMenuItem's submenus. Escape in a submenu closes only that submenu. - A menu bar. Hold
isOpenfor each menu and step between them withonStep. - The list renders nothing until the menu opens, so it costs nothing on the server.
Source code
If this page does not answer your question, have a look at the implementation of the component for more detail.