Menu
Transient surfaces that hold actions or detail. Both hide things behind a click, which is fine for the secondary and never acceptable for the urgent.
Overview
A menu holds actions. A popover holds content. A split button is a primary action with a menu of alternatives attached, and a menu button is a menu with no default. All four share one property that governs everything below: they are closed most of the time.
| Use | For | Never for |
|---|---|---|
| Menu button | A list of secondary actions with no obvious default | The primary action of a screen |
| Split button | A clear default plus alternatives — "Export PDF" with other formats | Actions of equal weight; that is a menu button |
| Popover | Supporting detail too long for a tooltip — a reference range, an explanation of a model's scope | Anything needed to act; anything requiring a decision |
| Dialog | — | See Dialog when a decision must be made before continuing |
Secondary only
Nothing behind a click is the only route to something a clinician needs. If it matters, it is on the screen.
Shallow
One level. Submenus cannot be held under interruption and are unreachable on touch with any reliability.
Yields
Positions itself clear of the alert region, closes on outside click and on Esc, and never blocks anything clinical.
Anatomy
Split button — a real default, alternatives attached.
Rules
- Never hide a primary action in a menu. The screen's main action is on the screen — see Button.
- Never put an alarm or a finding in either. Clinical signals are persistent and cannot live behind a click — see Alert Banner.
- Destructive items sit below a separator, last, and never adjacent to a routine action.
- Menu items are actions, not navigation. Places belong in Navigation; a menu that navigates loses the back button.
- No submenus. If a menu needs nesting it is a panel or a screen.
- Disabled items say why, inline — the same rule as Interaction states.
- Popovers close on outside click and on Esc, and never contain a form that submits. That is a Dialog.
- Repositions to stay clear of the alert region rather than overlapping it — see Elevation.
What may be inside
A menu holds actions. The moment it holds anything a clinician has to fill in, decide between, or read carefully, it has become a dialog wearing a menu's clothes — and it has none of a dialog's protections.
A text field, a date picker or a stepper inside a menu is offered by most systems and is refused here. A menu closes on an outside click, on Escape, and often on losing focus — all of which are correct for a list of actions and destroy half-entered input without warning. There is no confirmation step, no dirty-state prompt, and no room for the units, range or error text a clinical field requires.
If the reader must supply a value, that is a Dialog — which blocks deliberately, keeps what was typed, and can ask before discarding it.
- Every item is a verb. "Export report", "Compare with prior". An item that reads as a noun is usually a destination, and destinations belong in Navigation.
- Secondary text carries why an item is unavailable, or a keyboard shortcut — never a description. "Re-analyse · needs a new ECG" tells the reader something the label cannot. A second line explaining what the action does means the label failed.
- A shortcut shown in a menu works. Printing an accelerator that does nothing on this platform, or that a deployment has rebound, teaches the reader to distrust every other one.
- Beyond about eight items, the menu is the wrong shape. Systems commonly cap at fifteen and add a scrollbar; a scrolling menu is a panel that has not admitted it, and fifteen actions on one object is a design problem rather than a layout one. Group them onto a surface that can stay open.
- Disabled items stay, with their reason. Removing an unavailable action changes the set of things the reader believes exist — the same argument as a locked row in List.
Where it opens, and what it may cover
A menu is positioned relative to its trigger and flips to stay on screen. Two things it must never end up on top of.
- Never the alert region. Already the rule above, restated because automatic flipping is exactly how a menu ends up somewhere nobody chose — the flip logic knows about the viewport and not about the alarm layer, so the constraint has to be given to it explicitly.
- Never the row it acts on. A menu opened from a patient row that covers that row leaves the clinician choosing an action for a patient they can no longer see. It opens beside or below, and if there is no room for both, the row stays visible and the menu moves.
- The subject is named in the menu itself where the menu acts on one — a header reading "HARLAND, A. · MRN 44 812 907". A menu floating free of its context is how the right action reaches the wrong record, and it costs one line.
- It does not reposition while open. Content arriving behind it must not cause it to jump under a finger already moving — see Reprioritisation.
Opening and closing
- Focus moves to the first item on open, and arrow keys move between items — a menu is a composite widget, unlike an accordion.
- Escape closes it and returns focus to the trigger. Returning focus is the half that gets skipped: without it, Escape leaves a keyboard user at the top of the document with no way back to where they were.
- Closing is never destructive. Since a menu holds only actions, closing one can discard nothing — which is the same reason form controls are refused above.
- An outside click closes it and does nothing else. The click that dismisses a menu never also activates whatever was underneath.
Why there is no Popover
This page was titled “Menu & popover” for a long time, its sidebar entry said so until today, and its accessibility section credited a Radix Popover primitive. There has never been a Popover in this package. That is worth deciding out loud rather than quietly correcting, because it is the most obvious control this system does not have.
A popover is a container defined by nothing except being transient. Every rule this system would want to give it depends on what is put inside it — so the rules live on the specific surfaces, and the generic container is not built.
What a popover is usually reached for, and what to use instead:
| The need | Here |
|---|---|
| A short explanation of a control | Tooltip — supplementary only, never the sole carrier of anything, and dismissible without moving the pointer. |
| A set of actions on a thing | This component. The actions are data rather than children, so an alarm or a finding cannot be placed inside the surface. |
| A question that must be answered before continuing | Dialog — which blocks, and is required to offer a way out as well as a way on. |
| More detail about a value | A panel in the layout — see Grid & layout. Clinical detail worth reading is worth a place that does not vanish when the pointer moves. |
| A form in a small floating surface | Nothing. A form worth filling in is worth a region that survives an interruption, which is the one thing a transient surface guarantees it will not do. |
The pattern underneath is the one this page and Select both end on: a transient surface accumulates features because it is convenient to put them there, and each addition costs a protection the surface was never designed to provide. A component whose only definition is “floating” has no grounds to refuse any of them.
What the component enforces, and what it only asks for
The surface holds its own keyboard and dismissal. What it refuses to hold is content, and that refusal is the point.
| Rule | Held by | How |
|---|---|---|
| Focus moves into the menu when it opens | The component | The first enabled item is focused — written by hand after a gate found it missing entirely. Asserted: Menu: focus moves to the first item on open |
| Arrow keys, Home and End traverse it | The component | Including wrapping at both ends, and including unavailable items, which are announced as unavailable, described by their reason and do nothing when activated. They were native disabled and the keyboard stepped past them, so the reason reached pointer users only. Escape closes and returns focus to the trigger.Asserted: Menu: arrows reach an unavailable item, which says why and does nothing |
| An alarm or a finding cannot be placed inside it | The component | actions is data, not children. There is no slot to put a banner in.Asserted: Menu: actions are data, so a banner cannot be put inside the surface |
| Destructive items sort last, below a separator | The component | Ordering is applied by the component, not by the order you pass. Asserted: Menu: destructive actions are separated from routine ones |
| A disabled item says why | The component | note renders beside it, so a dead option is never a dead end.Asserted: Menu: its subject and unavailable reasons are visible when open |
| Type-ahead moves focus and never selects | The component | A printable key moves focus to the next available item whose label starts with it, wrapping, and skipping unavailable items as the arrows do. Nothing is selected by typing. Driven in a browser. Asserted: Menu: type-ahead moves focus to the matching item and selects nothing |
| The menu is described by what it acts on | The component | The subject line sits beside the menu and is its description. It was a paragraph inside role="menu", which a screen reader in a menu does not read. Read from the accessibility tree.Asserted: AX tree: the menu is described by what it acts on |
| The menu says what it acts on | You | subject is a string. Nothing checks that it names the row the menu was opened from, and a menu acting on something other than what the reader believes is the failure this page is about. |
| The list stays short enough to be scanned | You | Any number of actions is accepted. A menu that has grown a filter and a scrollbar is a panel that has not admitted it. |
A rule stated on a component page reads as a rule the component keeps. 5 of these 7 are; 2 are obligations this page was quietly handing to the reader. The component can stop a finding being put in a transient surface. It cannot stop the surface acting on the wrong subject.
Do's and don'ts
The primary action is on the screen; the menu holds the secondary ones.
The action the screen exists for, one click down. Under time pressure it may as well not be there.
Destructive item last, below a separator, visually distinct.
Permanent deletion between two routine actions, styled identically. One slip on a moving cart.
Supporting explanation. The finding and its confidence are already on the screen behind it.
Reliance information behind a click. On a touch display, at the moment of the decision, it is not there.
The menu names its subject. The right action reaching the wrong record costs one line to prevent.
Floating free of its context, and opened over the row it acts on. The clinician is choosing for a patient they can no longer see.
Opens a dialog with units, permitted range and confirmation.
The ellipsis says a decision is coming. The value is entered somewhere that can hold it.
A menu closes on an outside click, on Escape, and on losing focus. All three are correct for actions and all three silently discard what was typed.
Secondary text carries why it is unavailable. The item stays, so the reader knows the action exists.
A second line explaining what the action does means the label failed. Fix the label.
Accessibility
- Hand-written, not a primitive library. This line credited “Radix Menu and Popover primitives” for roles, focus trapping, outside dismissal and return focus. There is no Radix anywhere in this package, there is no Popover in it at all, and the focus-on-open and arrow-key handling this component does have were written by hand after a gate found they were missing. The same line claimed type-ahead, which is not implemented — and which Select argues is a hazard rather than a convenience.
- Trigger carries
aria-haspopupandaria-expanded; the surface is labelled by its trigger. - Arrow keys move between menu items, Home and End jump to the ends, type-ahead moves focus to the next item starting with the letter and selects nothing, Esc closes and returns focus.
- Split button is two controls with separate accessible names — "Export PDF" and "Other export formats" — not one control with a hidden second target.
- Items meet the physical touch floor, which on a cart means fewer visible at once — see Scaling.
- Never
role="menu"for navigation. A list of links is anav, and screen-reader users navigate them differently.
Outcomes of use
What this contributes to, in the terms of Usability & context of use. These are attributes believed to contribute to an outcome; the outcome itself is settled by observing real use in a specified context, not by this page.
- Effectiveness — the action taken is the one intended, and nothing needed is unreachable because it was hidden.
- Efficiency — secondary actions available without occupying screen space that clinical content needs.
- Satisfaction — confidence that nothing important is concealed. A clinician who opens every menu to check has been given the wrong container.
Clinical safety notes
Trace these in your risk file (ISO 14971) and usability engineering file (IEC 62366-1). Which of these the component keeps and which are yours is stated in the guarantees table; a control the component does not keep is one your risk file must show you keep.
- No alarm or finding inside a menu or popover. (the component) Mitigates: a clinical signal behind a click, invisible until sought.
- No primary action hidden. (you) Mitigates: delay in a time-critical pathway.
- Destructive items separated and last. (the component) Mitigates: irreversible action from a mis-tap on an adjacent item.
- Repositions clear of the alert region. (you) Mitigates: an open surface obscuring an active alarm.
- No submenus. (the component) Mitigates: structure that cannot be held under interruption or operated reliably by touch.
- Popovers never hold decision-relevant data. (the component; there is no popover) Mitigates: reliance information absent on touch surfaces and in print.
Implementation
$ npx shadcn@latest add https://md.notjustany.tech/r/menu.json
<Menu
triggerLabel="More"
subject="ECG acquired 14:26" // what the actions will act on
actions={[
{ id: "compare", label: "Compare with prior", onSelect: compare },
{ id: "reanalyse", label: "Re-analyse", disabled: true,
note: "Needs a new ECG" },
{ id: "discard", label: "Discard draft report", onSelect: discard,
destructive: true },
]}
/>
// 'actions' is data, not children, so an alarm or a finding cannot be placed
// inside the surface. Destructive items sort last.
Related
- Affordance & perceivability — why a right-click or an unlabelled icon is not a way to open this.
- Button — action hierarchy and destructive styling.
- Dialog — when a decision must be made before continuing.
- Tooltip — when a label is enough.
- Elevation — the popover layer.