Menu button
The one action menu of the app (AD + owner 28 Sep 2026: „sjednotit popover do menu-button"). A trigger plus a menu of actions — a labelled button „Přidat aktivitu ⌄", or the kebab „⋮" of a row, card or window. Popover is only the overlay layer underneath; a screen never composes an action menu from Popover + .acrm-popover-item itself. Picking a value is PickerMenu, not this.
When to use
- Several related actions share one entry point and none of them is the default: „Přidat aktivitu" → Schůzka, Úkol, Zaznamenat kontakt; „Nahrát audio" → the recording sources.
- The kebab „⋮" of a row, a card header, a record or a window (
iconOnly): Upravit, Kopírovat odkaz, Smazat. - The empty state needs such an action — pass it as
action.menuto EmptyState, never compose a menu under it by hand.
When not to use
- One action is clearly the default — use Split button.
- The items pick a value (view, stage, owner, filter) — use Picker menu, Select or Check menu.
- Only one action is available — pass one item; a labelled trigger becomes a plain Button that runs it. A kebab (
iconOnly, orforceMenu) still opens the menu: three dots must never run an unseen action (often „Smazat“). - The layer holds a form, a calendar or a preview — that is a Dialog, Picker panel or Entity preview, not a menu.
Anatomy
- Trigger — canonical
<Button>(default, primary or ghost) with an optional leading glyph, the label and a trailingchevron-down; or, withiconOnly, the canonicalIconButton(quietsm, oriconVariant="framed"in a window/toolbar; glyphdots-vertical⋮ unlessiconsays otherwise — one „more" symbol in the whole app, ⋯ is gone (owner 6 Oct 2026, acrm !4800)). WithiconOnlythelabelis not drawn but is required: it is thearia-labeland the title of the phone sheet.aria-haspopup="menu"/aria-expandedandstopPropagation(a kebab lives on clickable rows) are the component's job, never the host's. - Menu — DS Popover aligned to the start of the trigger, flipping to the end only when it would not fit (owner 26 Sep 2026, acrm !3903); on a phone a BottomSheet (grip, title = label, Hotovo, scrim).
- Row
.acrm-popover-item(role="menuitem"), 32px,500 13/20, ALWAYS ONE LINE (owner 28 Sep 2026, ACRM-375–377): the label sits in.nmand a long one ends with an ellipsis, the full text is the row'stitle; an action menu is capped atmin(360px, 100vw − --sp-5). Slots, in order: .ic— the glyph (icon) or a custom mark (iconNode: avatar, SVG);- label — bare text, or
.tx > .nmwhen something stands to the right; .acrm-kbd— a keyboard shortcut, hidden from screen readers (the label names the action);.chk— only forcheckedrows (menuitemradio),.chk.offkeeps the space. A row withhref(export, download, open in a new tab) is a real<a role="menuitem">—target,download, middle-click and „open in new tab“ keep working; a disabled one is a button withouthref.- Row variants:
.danger(irreversible action),.strong(the recommended one among equals),:disabledwith its reason intitle(tooltip +aria-describedby) — on the phone too, never a visible second line (owner 28 Sep 2026; the.acrm-popover-reasonline of acrm !4111 was removed). - Separator
.acrm-popover-sepbetween groups by intent. The separator before the first danger row is drawn by the component — the host never writes it; separators at the edges and doubled ones are dropped. - Empty / loading row
.acrm-popover-empty({ empty: true, label }) — one muted line („Načítám…", „Žádné podpisy"), not an item, never a heading; a menu with only this row still opens. - Submenu row (
submenu) — a trailingchevron-rightin the.drillslot (a glyph, not text; not.sub— a global side-menu class); it runs nothing, it opens the next level. Header (header) and switch row (switchOn) — see below.
Custom trigger
trigger({ open, menu, disabled, onClick }) (acrm !3899) swaps the drawn trigger for a host element that must still look right in the shape the surrounding row already owns — the component keeps deciding the menu, the items and the "one item = no menu" rule; the host only renders the trigger and wires onClick. Shipped shapes: a dialog's chip row (MetaChip, .tk-chip); a quiet rail row link („Nahrát audio ⌄", .mr-reclink, acrm !4014) — the host forwards aria-haspopup/aria-expanded itself there, since it owns the element. Use iconOnly, not a custom trigger, for a kebab.
forceMenu and danger items (2026-09-28, acrm !4088)
forceMenu keeps the menu even with a single item — for a ⋮ whose content changes with the screen (the bar's „Další akce" on a phone, a filter condition's „Odebrat filtr"), so one tap never runs an action the user did not see. danger: true on an item draws the red .danger row.
Submenu, header and switch (2026-09-28, owner-approved MR-B)
- Submenu (drill-in) —
{ key, label, icon, submenu: MenuButtonItem[] }. Choosing it swaps the content of the SAME panel for the submenu, topped by a back row (arrow-left+ the submenu's name) and a separator. On a phone it is the next step of the same sheet: the sheet title becomes the submenu's name and the back row says „Zpět". Closing the menu always returns it to the root. One level only. Pattern: mail „Přesunout do složky ›" (the folders arecheckedrows), reservation „Předat kolegovi ›". - Header —
header={…}: an identity block above the items (avatar = the sameUserAvatarface as the top bar, name, e-mail) with a separator under it, drawn only in the root. It is information („who am I signed in as"), not a heading and not an item. Only the account menu uses it. - Switch row —
{ key, label, icon, switchOn: boolean, onClick }: the canonical.acrm-switchon the right; a click toggles it and the menu stays open, so the result is visible at once (dark mode in the account menu). A setting with more than two values is not a switch — usecheckedrows or a submenu.
MenuPanel — the same menu without its own trigger (2026-09-30, acrm !4304)
MenuPanel (@acrm/ui) is the body of Menu button — rows, submenu, header, the phone sheet — without the trigger. The host owns the anchor and the open state: <MenuPanel anchorEl open onClose label items />. Use it only where the trigger cannot belong to the menu:
- the anchor is someone else's element (the phone menu of a preview chip);
- the menu is opened from an editor or another control, or has two triggers;
- the host needs to know the menu is open (items load only on open: template variables, bank „Přiřadit" suggestions).
The host draws its trigger with aria-haspopup="menu" and aria-expanded. label is the invisible menu name and the phone sheet title — short, no parenthetical explanation („Proměnné", not „Proměnné do pole Předmět (ukázkové hodnoty)"). Loading, error and empty states are { empty: true, label } rows. Closing by the host returns the menu to the root. A bare Popover with hand-made rows is never the answer — that is exactly what MenuPanel replaces.
Sizes
- The Button's height, or
IconButtonsm(24) / framed (32×30) foriconOnly; no size of its own.
Best practices
Do
- Keep the label a verb phrase true for every item („Nahrát audio", not „Soubor").
- Group by intent (plan vs. record) with a separator, never with a heading.
- Put irreversible actions last with
danger: true— the separator comes by itself. - Say why an item is disabled (
title) unless it is obvious from the screen. - Give the same action the same glyph and order on every surface — one shared list, not a copy per screen.
Don't
- Don't compose rows from
Popover+.acrm-popover-item— use Menu button, orMenuPanelwhen the host must own the trigger. - Don't make an item „dimmed but clickable" — an item is either active, or disabled with its reason.
- Don't let a row wrap to two lines, and don't add a grey note to it — no trailing tag (state, count, „Firemní", „brzy", „Přiřazeno"), no second line with a reason or a description. „Ty šedé texty jsou úplně k ničemu" (owner, 28 Sep 2026). The
tagprop and PickerMenudescno longer exist;validate-ds-usagerejects.tagin a menu row. - Don't open a second, nested popover from a row — a submenu (
submenu) swaps the content of the same panel. - Don't use a submenu for more than one level, or for 2–3 items that fit in the root.
- Don't make a bare ⋮ a direct action. The kebab means „more actions“ and never runs anything blind — with
iconOnly(orforceMenu) the menu opens even for a single item (AD, 28 Sep 2026). Collapsing „one item = a plain button“ holds only for a Menu button with a text label, where the action is visible. - Don't add a heading over the rows.
Content
- Label: verb, sentence case. Items: the full action („Nahrát soubor", „Z Plaudu"); a trailing „…" only when the action opens a further step („Sloučit s jinou osobou…"). One line each, no period.
- Empty row is a sentence: „Žádné podpisy", „Načítám…".
Related
- Button, Split button (shares the rows), Icon button (the kebab trigger),
MenuPanel(the same menu, host-owned trigger), Picker menu (pick a value), Popover (the overlay layer), Empty state. - Stories:
components--menu-button--default,--variants(kebab),--states,--anatomy,--submenu-header-switch,--composition.
Count, row actions and muted items (acrm !4527, 3 Oct 2026)
count(+countTone: 'accent') draws the canonical.acrm-countat the right edge of an item — use it for a number such as unread messages;kbdstays reserved for a keyboard shortcut.actions(+actionsLabel) adds a trailing ⋮ next to the row (wrapper.acrm-popover-row, never a button inside a button): clicking the row runs itsonClick, ⋮ drills the same panel into the row's actions with a back row named after it — for lists where a row opens a record and ⋮ manages it. On touch the ⋮ is always visible.muteddims only the item's glyph while the label stays readable — the same meaning asRecordOption.muted(e.g. an archived thread once group headings are gone).- First consumer: the assistant's thread history (groups by time are separated only by a divider, never headed). Story: Menu button › Row actions & count.
Source: docs/button/menu-button.usage.md · GitLab