Button
The commitment control. Everything a clinician does that changes the record, the workflow or the patient's pathway goes through one of these — so its hierarchy, its size and its wording are all safety decisions.
Overview
Buttons in consumer software optimise for conversion. In clinical software they optimise for the opposite: making sure the clinician who pressed it meant to. The system therefore constrains two things harder than a general-purpose library would — how many primary actions may exist (one) and how small a target may be (never below the touch floor).
Ranked
Exactly one primary action per view. If two things look equally important, the clinician has to decide which is — and that decision belongs to the designer, not the user.
Reachable
Sized for a gloved fingertip on a cart display, not a mouse on a desk. The floor is a physical measurement, not a pixel count.
Truthful
The label names the outcome, not the interaction. "Activate pathway", never "Submit" — a clinician should be able to predict the consequence without reading the surrounding screen.
Variants
Five variants, mapped onto shadcn's standard names so ecosystem examples drop in unchanged.
| Variant | Use | Per view | AcuteLine example |
|---|---|---|---|
default (primary) |
The one action the screen exists to enable | Exactly one | Activate chest-pain pathway |
outline (secondary) |
Supporting actions of equal legitimacy | 0–3 | Open ECG · Compare prior |
ghost (tertiary) |
Low-commitment or dismissive actions | Any | Defer · Close |
destructive |
Irreversible loss of data or work | Rare | Discard draft report |
link |
Navigation that leaves the current task | Any | View local guideline |
The brand coral #D8593A is not permitted on any button in a product interface.
At a glance it is not reliably distinguishable from the IEC high-priority alarm red, so a coral
primary button competes with a real alarm for exactly the attention the alarm needs.
--primary is teal. See Colour.
destructive is not an alarm
destructive is red because it destroys data. It shares a hue with
--alarm-critical and shares nothing else. A destructive button is a control the
clinician operates; an alarm is a signal about the patient. Never style an alarm as
a button, and never place a destructive button inside an alarm banner.
Sizes and touch targets
The pixel height of a button is meaningless on its own — what matters is the physical size it ends up on the target display. The system specifies both.
| Size | Height | Use | Touch permitted? |
|---|---|---|---|
sm | 32 px | Inside dense components — worklist rows, alert banners, toolbars | Mouse and keyboard only |
default | 36 px | Standard desktop workstation UI | Mouse and keyboard only |
lg | 44 px | Default for any touch surface — carts, bedside, gantry displays | Yes |
On any touch-operated display, the rendered touch target must measure at least 10 mm × 10 mm physically. 44 px satisfies this at typical workstation pixel densities, but on a high-PPI cart display it does not — which is why touch platforms apply an interface scale factor rather than hand-picking larger buttons. See Scaling & displays.
The visible button may be smaller than its hit area. A 32 px control may carry a 44 px hit area via padding — but never the reverse, because a target that looks bigger than it is produces mis-taps the clinician cannot explain.
States
- Focus is never removed. The ring is a governed token. A clinician on keyboard or a switch device must always be able to see where they are.
- Pending is a real state, not a spinner overlay. Any action that reaches a
server holds the button in
aria-busyand blocks re-submission. Double-activating "Notify cath lab" must be impossible, not merely discouraged. - Prefer explaining over disabling. A disabled button with no reason is a dead end. Where a precondition is unmet, keep the button enabled and explain on activation, or pair the disabled state with adjacent text saying what is missing.
Writing the label
- Verb plus object. "Activate pathway", "Notify cath lab", "Discard draft report". Never "OK", "Submit", "Yes".
- Match the heading. If a dialog asks "Discard this draft report?", the button says "Discard draft report" — not "Confirm".
- Sentence case, no full stop. Two to four words. Budget 35 % expansion for translation.
- Name the irreversible thing. A destructive label states what is lost, so the consequence survives being read at a glance.
Discard this draft report?
Your interpretation and annotations will be permanently lost.
Both labels name their outcome. Neither can be misread in a hurry.
Are you sure?
This action cannot be undone.
"Yes" to what? The destructive action is styled as primary, and neither label survives being read out of context.
Links, and the line between them
A link navigates. A button acts. The distinction is not stylistic: it decides which key operates the control, what the browser offers on a long press, what a screen reader announces, and whether the back button undoes it.
| Use | When | Consequences of getting it wrong |
|---|---|---|
<a href> |
The result is somewhere the user could have arrived at another way, and has a URL | A button that navigates cannot be opened in a new tab, cannot be copied, and is announced as an action that will change something |
<button> |
The result is a change — submitting, acknowledging, opening a dialog | A link that acts is activated by Enter but not Space, and is offered to the user as something safe to open in a new tab, which it is not |
- A link inside prose is underlined, not distinguished by colour alone — SC 1.4.1. Removing the underline and keeping the colour is the single most common accessibility regression in a redesign.
- The link text says where it goes. "Open the 21:00 observation", never "click here" and never a bare URL. A list of identical link names is unusable to anyone navigating by link — which is why Composed output requires each source binding to name what it opens.
- A link that leaves the application says so in its text. Not in a title attribute, not as an icon.
- Nothing opens in a new window without saying so, and nothing in a clinical workflow opens in a new window at all — a second window is a second place for a patient context to drift out of step, which is Patient header's whole subject.
- A link is never used to fake a button because it looked lighter. If the
visual weight is wrong, that is what
ghostabove is for.
Grouping and order
- Primary last, on the right in dialogs and forms; primary first on the left in toolbars and page headers. Be consistent within a product and document which you chose.
- Never two primaries. If a screen genuinely has two equal actions, both
become
outlineand the screen needs a clearer purpose. - Separate destructive actions physically — a gap, a divider, or the opposite side of the dialog — so a mis-tap on the adjacent control is not catastrophic.
- Never place a destructive button as the default focus target.
What the component enforces, and what it only asks for
A button is small enough that the type can hold nearly all of it. The exception is the one that matters when a button reaches a server.
| Rule | Held by | How |
|---|---|---|
| Never icon-only | The component | children is required, so a button with no text is not expressible.Asserted: Button: children are required, so an icon-only button is unwritable |
| A button that is off says why | The component | disabled is omitted from the props; unavailable takes a reason and renders it beside a still-reachable control.Asserted: Button: a button that is off says why, and stays reachable |
| A pending button names what it is waiting for | The component | pending requires a label, sets aria-busy and refuses activation.Asserted: Button: pending names what it is waiting for |
| The touch floor is the default | The component | size defaults to lg at 44 px; the smaller tiers are deliberate opt-ins.Asserted: Button: the documented sizes exist and default to the touch floor |
| Destructive intent is visible | The component | The destructive variant uses a fill token, never an alarm hue.Asserted: Button: destructive uses --destructive, never an alarm token |
| The action behind it is idempotent | You | The component can refuse a second activation while pending is set. It cannot make the request safe to send twice, and “notify the cath lab” sent twice is two calls. |
| One primary per decision | You | Nothing counts them. Two buttons of equal weight ask the reader to decide which is the ordinary path. |
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 make the pending state correct. Only your handler can make the request safe.
Do's and don'ts
One primary. The eye lands on the action the screen exists for.
Three primaries. The clinician now ranks them under time pressure, which is the designer's job.
Pending state blocks re-submission. One press produces one cath lab activation.
No pending state. On a display with 300 ms latency the clinician presses twice and the lab is paged twice.
Accessibility
- Use a real
<button>for actions and<a>for navigation. A div with a click handler is unreachable by keyboard and invisible to assistive technology. - Focus ring always visible at ≥ 3:1 against both the button and the
surface behind it. Never
outline: none. - Icon-only buttons require an accessible name via
aria-label, plus a tooltip for sighted users. An icon alone is never a label. - Pending state announces.
aria-busy="true"and a live-region update, so a screen-reader user knows the action was received. - Do not rely on colour alone to mark a destructive action — the label carries the meaning.
- Target spacing. At least 8 px between adjacent touch targets so the gap itself is not tappable.
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 action intended — one primary per view, labels that name outcomes, destructive controls physically separated.
- Efficiency — Physical effort to acquire the target. A mis-tap costs a full recovery cycle, which is why the touch floor is stated in millimetres rather than pixels.
- Satisfaction — Confidence that pressing will do what the label says, particularly immediately before an irreversible step.
Clinical safety notes
Trace these in your risk file (ISO 14971). 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.
- One primary per view. (you) Mitigates: the wrong action taken under time pressure because two looked equally weighted.
- 10 mm physical touch floor. (the component) Mitigates: mis-tap on a cart or gantry display, particularly with gloves or in motion.
- Pending blocks re-submission. (the component) Mitigates: duplicate irreversible actions — two cath-lab activations, two orders.
- Destructive actions physically separated and never default-focused. (you place it; the component marks it) Mitigates: accidental data loss from keyboard-confirm or adjacent mis-tap.
- Labels name outcomes. (you) Mitigates: confirmation-dialog blindness, where "Yes/No" is answered without reading.
- No coral in product. (the component) Mitigates: a brand-coloured control being read as a high-priority alarm, and dilution of alarm salience.
Implementation
Button used to ship inside the toolbar install command, and this page offered no install command at all — it described variants, sizes, states and grouping, and told nobody how to get any of it. It is its own item now. The toolbar depends on it, so installing the toolbar still brings its buttons.
$ npx shadcn@latest add https://md.notjustany.tech/r/button.json
<Button variant="primary">Notify cath lab</Button>
<Button unavailable={{ reason: "Needs a completed ECG" }}>
Re-analyse
</Button>
<Button pending={{ label: "Notifying cath lab…" }}>
Notify cath lab
</Button>
// No cva, no buttonVariants, and no development-time warning about
// multiple primaries — this page described all three and none exists.
// The variants are a plain lookup; the constraints are in the type:
// children are required, disabled is not a prop, and pending needs its
// label.
Grouping related actions
ButtonGroup ships with this install command and was documented on
Toolbar, which installs from a different one. A reader who ran
add button got a component their page never named.
A button group is a set of related actions, not a segmented control. If the buttons are mutually exclusive states rather than actions, that is a Radio group — and rendering it as buttons hides the fact that one of them is already true.
- The group carries a name. A
role="group"with no label is a fence around three controls with nothing said about why they are together. - One primary at most. Two buttons of equal weight beside each other ask the reader to decide which is the ordinary path, which is the page's job, not theirs.
- A destructive action is not grouped with routine ones. Adjacency is what makes a mis-tap possible; see Perceivable affordance.
<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>
Related
- Alert Banner — buttons inside an alarm use
sm. - Colour — why primary is teal and not coral.
- Typography — label sizing and expansion budget.
- Dialog — destructive confirmation patterns.