Button

Three variants, five tones, three sizes. Every value is a token; the component file contains no colour, and every label is measured against its fill in the margin.

Try it

Click it — the label keeps its place, so the button does not change width.

Choosing a variant and a tone

The variant sets how much a button asks for. Solid fills the capsule and is the loudest thing on a surface; outline draws the edge and paints the label; ghost paints the label alone and takes the wash on hover. The tone says what the action means, and only the solid variant carries all five.

One solid accent per view. It is the action the view exists for — confirm the booking, save the record — and a second one beside it makes the reader choose between two things that both claim to be the point. The alternative next to it is neutral: the same size, the same capsule, and a fill that sits a step from the surface rather than on the accent ramp. Everything further down the hierarchy is outline or ghost.

Danger is for the action that destroys something, and it is solid only when that action is the point of the view, as in a confirmation. In a row of a table it is ghost, so a list of records is not a list of red buttons. Success is the confirming step of a flow and nothing else; a button is not a status. Tertiary is the highlight — the gold the peaks take before they turn pink — for the one action a product wants to draw the eye to that is not the primary one, and it is never a state.

Two solid accents in one row.

A dialog footer with Save draft and Confirm booking both filled reads as two primary actions, and the reader has to work out from the words alone which one the dialog is for. The neutral fill beside the accent is what makes the accent read as primary across the room.

Variants and tones

Tertiary and success are solid-only, and the type signature enforces it.

Outline and ghost paint the tone as text, and outline as a border too. Tertiary cannot be painted that way: its fill, flare/400, is 2.53:1 on white, and the theme has no tertiary text colour — text/tertiary is a level of the text hierarchy, not this tone, and flare/700 has not been drawn as one. So variant="outline" tone="tertiary" does not compile rather than producing a button nobody should ship.

Success was left out for the same reason, until field validation added text/success, at 7.20AAA on the canvas, and border/success, at 4.74AA. An outline success button could pass now. It stays out because it was never drawn.

Sizes and anatomy

Three heights, shared with Input, Select and the date field, so a button and a field in one row sit on one line. The capsule is the whole shape: the radius is the largest the scale has, so it follows the height. The gap between an icon and the label is 8px at every size — a smaller button does not get a tighter icon.

Button sizes
SizeHeightInline paddingLabelIcon
sm32px16pxbutton/md · 14/1816px
md40px20pxbutton/md · 14/1816px
lg48px24pxbutton/lg · 16/2020px

Labels

A label starts with the verb and names the action: Confirm booking, Save draft, Cancel appointment. Sentence case, no full stop, no exclamation. A button that confirms a consequence repeats it, so the dialog is answerable without reading its body: under Cancel this appointment? the buttons are Cancel appointment and Keep it, never Yes and No, and never a bare Cancel that could mean either.

An icon goes at the start when it carries meaning the label alone would not — a plus for creating, a bin for deleting — and at the end when it points where the action goes. Icons are decoration to a screen reader, so a button with an icon and no label needs an aria-label, and it needs one the same way a labelled button reads: the verb and the object.

fullWidth is for a form on a phone, where the one primary action spans the column inside the layout's margins. On a desktop a button is as wide as its label, and a row of them is as wide as its labels.

States

Hover and pressed are two steps up the tone’s own ladder for the filled tones, and the wash — mist/500 at a low alpha over whatever is beneath — for neutral, outline and ghost. Pressed also gives way by four percent, and colour alone under reduced motion. Disabled keeps the shape and drops the paint; it is the one state with no hover.

Loading keeps the tone

A loading button is disabled — it must not be activated twice — but busy and unavailable are different states and do not look alike. It holds its own fill and label colour, and the spinner takes that label colour, so every tone stays legible while it works.

Accessibility

Focus adds a two-pixel ring at a two-pixel offset rather than recolouring the border, so colour is never the only channel carrying the state. Tab through the examples above to see it.

Every label clears AA on its fill in both modes; the ten readings are in the margin, and the suite fails if one falls. The dark labels are not white: the accent fill lightens across hover and pressed there, and a white label would lose at the hover step, so the label darkens to keep clear of it.

The small button is 32px tall, above the 24px WCAG asks of a target at AA. On a screen that is touched, use the large one: 48px is the size a thumb lands on, and a row of small buttons on a phone is a row of near misses.

Loading sets aria-busy and disables activation. The label stays in the document and loses only its paint, which keeps the accessible name stable and stops the button resizing mid-action.

Icons are marked aria-hidden, so the accessible name comes from the label alone. An icon-only button needs an explicit aria-label.

The element defaults to type="button". A bare button inside a form defaults to submit, which turns a decorative button into an accidental form submission.

Props

Button props
PropTypeDefault
variant'solid' | 'outline' | 'ghost''solid'
tone'accent' | 'neutral' | 'tertiary' | 'success' | 'danger''accent'
size'sm' | 'md' | 'lg''md'
loadingbooleanfalse
iconStartReactNode
iconEndReactNode
fullWidthbooleanfalse

All remaining button attributes are passed through.