Design system/Components
Menu and action sheet
ActionMenu is Flowra's "•••" menu: a dropdown under the button on desktop, and an action sheet with finger-sized rows on phones.
In codecomponents/knowledge/ui.tsxapp/globals.css (.sheet-menu)
Live example
Tap the button. If you shrink the window to phone width, the same menu rises from the bottom.
Row anatomy
- IconOptional, 15px.
- TagWhat the row does.
- AboutA quiet second line, when the meaning of the choice isn't obvious.
- Trailing noteA shortcut or "Soon".
- Divider and heading"sep" and { heading } separate groups.
Bottom sheet
Every phone menu in Flowra is a .sheet-menu: from the bottom, up to 72 percent of the screen height, 20px top corners, a 38 by 4 handle that says it's a sheet, and 48px rows with a 14 font. Its bottom leaves room for the device's safe area.
72vh20px 20px 0 038 × 4pxmin-height 48px · 14px0.24s cubic-bezier(0.32, 0.72, 0.25, 1)Rules
Do
Dangerous actions go at the bottom, with danger, followed by a confirmation dialog.
Don't
Deleting from a menu row without confirmation.
Do
Show the current state with active.
Don't
A hand-made checkmark next to the label.
Do
A menu for infrequent actions on one thing.
Don't
Hiding the page's main action behind •••.
<ActionMenu
title={tr("صفحه")}
items={[
{ key: "copy", label: tr("کپی پیوند"), icon: paths.link, onSelect: copy },
"sep",
{ key: "del", label: tr("حذف"), icon: paths.trash, danger: true, onSelect: ask },
]}
trigger={({ open, ref }) => (
<button ref={ref} className="icon-btn" onClick={open} aria-label={tr("بیشتر")}>
<Icon d={paths.more} size={16} />
</button>
)}
/>On desktop
232pxprop width360px6px8px from the screen edgesdropIn 0.2s cubic-bezier(0.34, 1.56, 0.64, 1)padding 10px 12px · radius 9px · 14px / 500Phone ≥ 46px15px · opacity 0.811px · --text-secondary11px · --text-tertiary10.5px / 700 · --text-tertiary1px --border · margin 4px 0z-index 1250Row states
--muted--accent on --accent-softWith desc: check 14pxcolor: var(--red)5.14 : 1 · 4.55 : 1 on card; AAopacity 0.452px solid var(--accent) · offset 2pxAccessibility and behavior
- The container is role="menu" with aria-label set to title; rows are role="menuitem", or role="menuitemradio" with aria-checked when they're active, and the divider is role="separator". The trigger button automatically gets aria-haspopup="menu" and aria-expanded.
- When it opens, focus moves to the first enabled row. Up and down arrows move between rows, Home and End jump to the first and last, letters typed within 600 milliseconds jump to the next row that starts with them, and Enter or Space activates the row. Rows have tabIndex=-1 and focus is moved by script; Tab closes the menu.
- Escape closes the menu and returns focus to the trigger button. A mousedown outside the menu and the button, and on desktop a page scroll, close the menu. Scrolling inside the menu itself doesn't close it.
- When a row is selected, the menu closes first and then onSelect runs, so a confirmation dialog that opens doesn't sit on top of a leftover menu.
- On phones the backdrop is rgba(0, 0, 0, 0.38) and tapping it closes the menu. The 38 by 4px handle is only a cue; the menu sheet isn't draggable.
- In right-to-left the menu aligns with the button's right edge, and in left-to-right with its left edge.
- The delete red on the card is 5.14 to 1 in light and 4.55 in dark, AA in both; the trash icon and the "Delete" label also convey the meaning without color.
Do
A title for every menu; on phones it's the sheet heading.
Don't
A sheet with no heading that doesn't say whose menu it is.
Do
About seven rows at most; beyond that, headings and dividers.
Don't
A long menu that goes past 360px on desktop and scrolls.
Related pages