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.
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.
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).
- One primary per view. Mitigates: the wrong action taken under time pressure because two looked equally weighted.
- 10 mm physical touch floor. Mitigates: mis-tap on a cart or gantry display, particularly with gloves or in motion.
- Pending blocks re-submission. Mitigates: duplicate irreversible actions — two cath-lab activations, two orders.
- Destructive actions physically separated and never default-focused. Mitigates: accidental data loss from keyboard-confirm or adjacent mis-tap.
- Labels name outcomes. Mitigates: confirmation-dialog blindness, where "Yes/No" is answered without reading.
- No coral in product. Mitigates: a brand-coloured control being read as a high-priority alarm, and dilution of alarm salience.
Implementation
The stock shadcn Button with two changes: an enlarged lg size that is the default
on touch platforms, and a development-time warning when more than one primary renders in a tree.
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-sm " +
"text-sm font-semibold transition-colors focus-visible:outline-none " +
"focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 " +
"disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
// NB: no coral. --primary is teal by design — see Colour.
default: "bg-primary text-primary-foreground hover:brightness-110",
outline: "border border-border bg-transparent hover:bg-muted",
ghost: "hover:bg-muted",
destructive: "bg-destructive text-destructive-foreground hover:brightness-110",
link: "underline-offset-4 hover:underline text-primary",
},
size: {
sm: "h-8 px-3", // pointer input only
default: "h-9 px-4", // pointer input only
lg: "h-11 px-5", // touch floor — default on touch platforms
icon: "h-11 w-11", // always touch-sized; requires aria-label
},
},
defaultVariants: { variant: "default", size: "lg" },
}
)
// Pending state — blocks re-submission of irreversible actions.
<Button
variant="default"
size="lg"
disabled={isPending}
aria-busy={isPending}
onClick={notifyCathLab}
>
{isPending ? "Notifying…" : "Notify cath lab"}
</Button>
Props
| Prop | Type | Default | Notes |
|---|---|---|---|
variant |
default | outline | ghost | destructive | link |
default | One default per view. |
size | sm | default | lg | icon |
lg | Defaults to the touch-safe size deliberately. |
asChild | boolean | false |
Radix Slot — render as <a> for navigation. |
disabled | boolean | false |
Pair with adjacent text explaining what is missing. |
aria-busy | boolean | — | Required for any action that reaches a server. |
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.