Toolbar
Two ways of putting actions next to each other, with opposite meanings. A button group says these are alternatives. A toolbar says these apply to what you are looking at. Confusing the two is how a clinician ends up applying an action to the wrong patient.
Overview
Both components exist to make a relationship visible without words. Adjacency is the whole message, so it has to be truthful: things touching each other are read as belonging to the same decision, and things in a bar above a table are read as acting on that table.
The failure mode is the same for both — an action whose target is ambiguous. In a clinical worklist that ambiguity is not cosmetic. "Acknowledge" in a toolbar that appears to float between two panels is an acknowledgement of something, and the clinician has to guess what.
Adjacency means something
Buttons only touch when they are alternatives in one decision. Unrelated actions are separated by at least a group gap.
The target is visible
A toolbar sits inside the boundary of the thing it acts on, and names it when the thing is a selection rather than the whole view.
Nothing hides on hover
Actions are present or absent, never revealed by pointing. See Interaction states.
Which one
| Button group | Toolbar | |
|---|---|---|
| Says | These are alternatives — pick one, or one mode is active. | These act on the region I sit in. |
| Spacing | Segments share borders; zero gap. | Grouped with gaps and separators; never a continuous bar of segments. |
| Selection | May be stateful (aria-pressed / radio semantics). |
Never stateful as a whole; individual toggles may be. |
| Max items | Five. Beyond that use Select. | No fixed limit, but overflow goes to a Menu, not a scrollbar. |
| Keyboard | Arrow keys move within the group; one tab stop. | Arrow keys move within the toolbar; one tab stop. |
Anatomy
Waveform region. The toolbar is inside the same bordered panel, so its scope is unambiguous.
| Part | Rule |
|---|---|
| Scope label | The toolbar names what it acts on when that is not obvious from position — the recording, the selection, the patient. |
| Groups | Related controls sit together; unrelated groups are separated by a separator or by at least 16 px. Never both. |
| Separator | Decorative, aria-hidden. Semantics come from nested
role="group", not from the line. |
| Primary action | At most one, pushed to the trailing edge. Everything else is ghost or secondary — see Button. |
| Overflow | A trailing menu button. Destructive and irreversible actions never live in overflow. |
Button group
Segments share edges because the choice is exclusive. If two adjacent buttons can both be pressed, or if pressing one does not deselect the other, it is not a button group — it is two buttons that have been glued together by mistake.
The pressed
segment is filled and carries aria-pressed. Fill alone would fail
greyscale and colour-vision checks.
- Never mix modes and commands. A group containing "0/1 h", "0/2 h" and "Export" has one button that does not belong; it is the one that will be pressed by accident.
- Never a split button for a clinical commit. The default half of a split button is invisible until pressed. See Menu.
- Five segments maximum. Past that, targets shrink below the 24 px minimum on narrow viewports and the group starts to reflow.
- The group never wraps. A wrapped segmented control reads as two separate controls with two separate selections.
Split buttons, and when not to
A split button pairs a default action with a menu of alternatives behind an attached control. It is a space-saving device with a specific hazard: the two halves are adjacent, differ by a few millimetres, and do different things — which is the adjacent-control error that Touch & gestures names as the dominant touch failure in gloved use.
A split button is permitted only when every action behind the menu is reversible and non-clinical, and its default half is never destructive. Export formats, print options, sort orders. Not "Discharge", not "Sign", not anything that changes what a device does or what a record says.
- Both halves carry a 44 px target and are separated by a visible divider. Where the surface is gloved, the whole construct becomes two separate buttons instead.
- The default half states what it does — "Export CSV", not "Export" with the format hidden in the menu. A default action nobody can name from the label is a default action that gets pressed by accident.
- Choosing from the menu does not change the default for next time. Silent adaptation means the same press produces different results on different days, which is the one property a repeated action must not have.
- The menu half is a separate control with
aria-haspopupand its own accessible name — "More export formats" — not an unlabelled chevron sharing the button's name.
Scope and selection
A toolbar action that operates on a selection states the count and the
subject in the button itself — Acknowledge 3 selected, not
Acknowledge. A count that changes under the clinician's hand while the pointer is
travelling toward the button is the classic wrong-target error, and the label is the only thing
that catches it.
Acknowledge is deliberately absent. Acknowledging an alarm is a per-patient act with per-patient consequences — see Acknowledge & escalate.
- Bulk acknowledgement does not exist. No toolbar in this system offers it, at any priority, for any selection size.
- The selection toolbar replaces nothing. It appears in addition to the view toolbar rather than swapping it out, so the clinician does not lose the controls they were using.
- Selection survives a sort. If it cannot, the toolbar clears it explicitly and says so, rather than silently acting on different rows.
States
| State | Rendering |
|---|---|
| Unavailable | Disabled with a reason adjacent, or removed entirely. Never a disabled control with no explanation — see Button. |
| Pending | The pressed control shows activity in place and keeps its label and width. The rest of the toolbar stays live. |
| Pressed (group) | Fill, weight and aria-pressed. Three signals, one of which is
non-visual. |
| Overflowed | Items move into the menu from the trailing end in a fixed order, so the same action is always in the same place at a given width. |
| Read-only context | The toolbar renders with commands removed, not disabled. A row of greyed buttons reads as a fault. |
A toolbar that changes shape
Mainstream systems offer several ways for a toolbar to rearrange itself: a tool picked from the overflow menu takes the place of the last tool in its group; a tool with a nested menu is replaced by whichever variant is chosen from it; tools can be dragged into a new order; the whole bar can be floated and docked wherever the reader likes. Each is offered as configurability. Together they mean the button under your finger is not the button that was there last time.
Using a toolbar never rearranges it. Position is how a control is found once it is known — not read, not searched for, reached. A toolbar that reorders as a side effect of being used destroys the only property that made it fast.
| Offered elsewhere | Verdict here |
|---|---|
| A tool chosen from the overflow menu replaces the last tool in its group | No. It evicts a control the clinician did not ask to lose, and shifts everything after it. The tool they wanted is now in the bar and the one they rely on is in the menu, and nothing said so. |
| A nested tool is replaced by the variant chosen from its menu | No. The same defect with a smaller radius. A control that becomes a different control keeps its position and changes its meaning, which is worse than moving. |
| Drag to reorder | No. On a shared workstation the arrangement is inherited from whoever was here last, and there is no keyboard equivalent of a drag. |
| Float the toolbar anywhere; dock it by dragging within 16 px of an edge | No. A floating bar covers the thing it operates on, a 16 px snap is a fine-motor task nobody performs in gloves, and the position is one more piece of state a colleague set. |
| Collapse to icons to save space | No. There is no icon set here. A tool is a word, and a bar with no room for words has too many tools — see Which control is this. |
| Truncate a label that exceeds the tool width | No. A truncated verb is not a verb. Per Perceivable affordance, a control the reader cannot read is a control they cannot use. |
What remains permitted is deliberate and rare: the set of tools may differ by view, because a different view is a different task; and it may differ by role, because a control someone may not use is better absent than disabled. Both are decided before the clinician arrives, not by what they did a moment ago.
Calipers · Measure · Compare · More
after using Annotate from More: Calipers · Measure · Compare · More
The bar is where it was. Annotate ran; nothing moved to make room for it.
Calipers · Measure · Compare · More
after using Annotate: Calipers · Measure · Annotate · More
Compare has gone into the menu to make room. The next reach for the third button lands on a tool that draws on the study.
What the component enforces, and what it only asks for
The keyboard contract is held — it was not, until an interaction test was written for it. Everything about what is in the bar is yours.
| Rule | Held by | How |
|---|---|---|
| One tab stop, arrows inside | The component | A roving tabIndex, so twelve controls cost one tab press rather than twelve. This was a plain div announcing itself as a toolbar until the interaction gate was pointed at it.Asserted: Toolbar: one tab stop, not one per control |
| Arrow direction follows the reading direction | The component | Mirrored under dir="rtl", so the control to the right of the pointer in Arabic is the one the left arrow reaches.Asserted: Toolbar: arrow keys move between controls |
| Re-entering returns to the last control used | The component | The tab stop follows focus, so leaving and coming back does not restart at the beginning. Asserted: Toolbar: re-entering returns to the last control used |
| Separators and spacers are not focusable | The component | Both are inert furniture; neither is a control. Asserted: Toolbar: a separator is furniture, not a tab stop |
| The bar does not rearrange itself | You | The children are yours. Nothing here reorders, but nothing stops you rebuilding the bar with the tool just used moved to the front. |
| An action states what it acts on | You | “Acknowledge 3 selected” against “Acknowledge” is a label you write. |
| No bulk acknowledgement | You | Prohibited system-wide, and expressible here as an ordinary button. |
A rule stated on a component page reads as a rule the component keeps. 4 of these 7 are; 3 are obligations this page was quietly handing to the reader. Position is how a control is found once it is known. The component will not move anything; it also cannot stop you.
Do's and don'ts
Modes touch each other; a command sits past a separator. The spacing tells you which is which before you read a word.
A command welded into a segmented control. It looks like a third view mode, and it is one segment away from the pointer that was heading for 3 × 4.
Count in the label as well as in the bar. If the selection changes while you reach for it, the button says so.
An unscoped verb and a bulk acknowledgement. Neither says what it acts on, and one of them dismisses alarms the clinician has not read.
One primary, at the trailing edge, visually separated from the exploratory controls it is not related to.
Three primaries. Emphasis that applies to everything applies to nothing, and the highest-consequence action no longer stands out.
Accessibility
- One tab stop per toolbar or group. Tab enters and leaves; arrow keys move inside. A toolbar of twelve controls must not cost twelve tab presses on the way to the waveform.
- Roving tabindex — the last-focused control keeps focus when the toolbar is re-entered.
role="toolbar"with anaria-labelnaming the scope, andaria-controlspointing at the region it acts on.- Groups inside a toolbar use
role="group"with their own label. The visual separator isaria-hidden. - Toggle state is
aria-pressed, never colour alone and never a class name. - Targets stay ≥ 24 px with ≥ 24 px spacing between adjacent clickable centres (WCAG 2.2 SC 2.5.8). Segments that share a border still need real height — 44 px in gloved-use contexts, see Density & scaling.
- Overflow is reachable by keyboard at every viewport width, and its contents are announced as a menu, not as a dialog.
- Never
aria-disabledwithout an explanation in the accessible name or a linked description.
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 lands on the thing the clinician believed they had selected.
- Efficiency — one tab stop rather than a dozen; grouping that answers "which of these is a mode?" without a click.
- Satisfaction — controls that stay where they were. A toolbar that rearranges itself between visits is a small, repeated cost to trust.
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.
- Scope named in the toolbar and in the action label. (you) Mitigates: an action applied to the wrong patient or the wrong recording.
- Selection count restated in the button. (you) Mitigates: a selection that changed between intention and click.
- No bulk acknowledgement anywhere. (you) Mitigates: unread alarms cleared as a batch.
- Destructive and irreversible actions never in overflow. (you) Mitigates: a high-consequence action reached by accident from a collapsed menu.
- Fixed overflow order. (you) Mitigates: motor memory landing on a different action at a different window width.
- Modes and commands never share a segmented control. (you) Mitigates: a command fired while switching views.
- Commands removed rather than disabled in read-only contexts. (you) Mitigates: a legitimate permission state read as a system fault.
Implementation
$ npx shadcn@latest add https://md.notjustany.tech/r/toolbar.json
<Toolbar label= "ECG review actions" >
<ButtonGroup label= "Lead layout" >
<Button variant= "outline" aria-pressed={layout === "12x1" }>12 × 1</Button>
<Button variant= "outline" aria-pressed={layout === "6x2" }>6 × 2</Button>
</ButtonGroup>
<ToolbarSeparator />
<Button variant= "outline" >Compare with prior</Button>
</Toolbar>
// One tab stop; arrow keys move between the controls and follow the
// reading direction. A ButtonGroup is related actions — if the buttons are
// mutually exclusive states, that is a RadioGroup.
What the component takes. This table used to list scope, controls, overflowFrom, ButtonGroup — props the component does not have, and never had. Found by the registry gate’s props-table rule.
| Prop | Type | Default | Notes |
|---|---|---|---|
label | string |
Required. Accessible name of the toolbar. | |
children | React.ReactNode | — | Required. The controls. One tab stop for all of them; arrow keys move between them. |
Separator and spacer
Two pieces ship with this install command and neither was named on this page.
ToolbarSeparatordraws the rule between groups and isaria-hidden: it is a visual boundary, and the grouping it depicts is carried for everyone else by therole="group"and its label.ToolbarSpacerpushes what follows to the far end of the bar. It uses a logical margin, so in a right-to-left layout “the far end” mirrors rather than staying on the side an English reader expects.- Neither is focusable and neither is a control. A separator that can be tabbed to is a tab stop that does nothing, which is the cost of building furniture out of buttons.
Related
- Button — hierarchy, disabled reasoning, target sizes.
- Menu — where overflow goes, and why split buttons are banned for commits.
- Data grid — selection semantics the toolbar reports.
- Acknowledge & escalate — why bulk acknowledgement is absent.
- Spacing — the group gap that carries the meaning.