Components · Actions

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.

Stable · v1.0 shadcn/ui IEC 62366-1 WCAG 2.2 AA

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.

All variants · size: default
View guideline
VariantUsePer viewAcuteLine example
default (primary) The one action the screen exists to enable Exactly oneActivate chest-pain pathway
outline (secondary) Supporting actions of equal legitimacy0–3Open ECG · Compare prior
ghost (tertiary) Low-commitment or dismissive actionsAnyDefer · Close
destructive Irreversible loss of data or workRareDiscard draft report
link Navigation that leaves the current taskAnyView local guideline
Governed — no coral

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: sm · default · lg
sm · 32 px
default · 36 px
lg · 44 px
SizeHeightUseTouch permitted?
sm32 px Inside dense components — worklist rows, alert banners, toolbars Mouse and keyboard only
default36 px Standard desktop workstation UIMouse and keyboard only
lg44 px Default for any touch surface — carts, bedside, gantry displays Yes
Governed — 10 mm touch floor

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

Interaction states · primary
default
hover
focus-visible
disabled
pending

Writing the label

Do

Discard this draft report?

Your interpretation and annotations will be permanently lost.

Both labels name their outcome. Neither can be misread in a hurry.

Don't

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.

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.

UseWhen 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

Grouping and order

Action row · one primary

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.

RuleHeld by How
Never icon-onlyThe 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 whyThe 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 forThe 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 defaultThe 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 visibleThe 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 idempotentYou 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 decisionYou Nothing counts them. Two buttons of equal weight ask the reader to decide which is the ordinary path.
Why this is written down

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

Do

One primary. The eye lands on the action the screen exists for.

Don't

Three primaries. The clinician now ranks them under time pressure, which is the designer's job.

Do

Pending state blocks re-submission. One press produces one cath lab activation.

Don't

No pending state. On a display with 300 ms latency the clinician presses twice and the lab is paged twice.

Accessibility

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.

Clinical safety notes

Risk controls this page specifies

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.

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.

The rule

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.

<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>
NotJustAnyMed.Tech Design System · Button · v1.0 · draft for review
Reference applications named in this system are fictional; all patient data shown is fabricated.