Toolbar & button group
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 & popover.
- 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.
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. |
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).
- Scope named in the toolbar and in the action label. Mitigates: an action applied to the wrong patient or the wrong recording.
- Selection count restated in the button. Mitigates: a selection that changed between intention and click.
- No bulk acknowledgement anywhere. Mitigates: unread alarms cleared as a batch.
- Destructive and irreversible actions never in overflow. Mitigates: a high-consequence action reached by accident from a collapsed menu.
- Fixed overflow order. Mitigates: motor memory landing on a different action at a different window width.
- Modes and commands never share a segmented control. Mitigates: a command fired while switching views.
- Commands removed rather than disabled in read-only contexts. Mitigates: a legitimate permission state read as a system fault.
Implementation
$ npx shadcn@latest add https://md.notjustany.tech/r/toolbar.json
// Scope is a required prop, not a convention. A toolbar that does not
// know what it acts on cannot label its actions.
<Toolbar
label="ECG review actions"
scope={{ kind: "recording", id: "ecg-14263800", display: "ECG 14:26:38" }}
controls="waveform-region"
>
<ButtonGroup label="Lead layout" value={layout} onChange={setLayout}>
<ButtonGroup.Item value="12x1">12 × 1</ButtonGroup.Item>
<ButtonGroup.Item value="3x4">3 × 4</ButtonGroup.Item>
</ButtonGroup>
<Toolbar.Separator />
<Toolbar.Action onSelect={openCalipers}>Calipers</Toolbar.Action>
</Toolbar>
// A selection-scoped action must interpolate the count, so the label
// cannot silently disagree with the selection.
if (scope.kind === "selection" && !/\{count\}/.test(action.label)) {
throw new Error("[Toolbar] selection-scoped actions must include {count} in the label.")
}
| Prop | Type | Notes |
|---|---|---|
label | string |
Required. Accessible name of the toolbar. |
scope | ToolbarScope |
Required. view · selection ·
record · recording. |
controls | string |
Id of the region acted on. Emits aria-controls. |
overflowFrom | number |
Index past which items collapse. Order is fixed, never reflowed by priority. |
ButtonGroup value | string |
Controlled, exclusive. Multi-select is not supported by design. |
Related
- Button — hierarchy, disabled reasoning, target sizes.
- Menu & popover — 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.