atom · stable · site, app, extension · since 2026-08
Button
Purpose
An action or a link, in one component that renders a when there is a destination and button when there is an action. The distinction is what matters for the keyboard and for assistive technology, and it is the one lost by styling a div.
When to use, when not
Use it for everything a user activates. Give href for a destination, nothing for an action. Do not use it for a toggle that has a pressed state (that needs aria-pressed, a different component), and do not put an icon alone in it without a visible or hidden word.
Anatomy
- The element:
aorbutton,type="button"by default so it never submits a form by accident. - The label, always a verb.
- The busy ring, added beside the label when
busyis set, under motion only. - With
reason, the sentence next to the unavailable button, tied byaria-describedby.
Props
| Name | Type | Default | What it does |
|---|---|---|---|
variant? | 'primary' | 'secondary' | 'ghost' | 'destructive' | 'link' | 'primary' | The role on the page; destructive borrows the non-conformity red because it shares its meaning. `link` is an action that reads as a link in a sentence ("Renommer", "Modifier"): underlined, no box, the text's own size. Still a button: a destination takes the Link component. |
size? | 'sm' | 'md' | 'lg' | 'md' | `sm` for toolbars and table rows, `lg` for a page's one call to action. |
fullWidth? | boolean | false | Takes the whole width: narrow forms, panels. |
busy? | boolean | false | The requested work is in progress. aria-busy and aria-disabled, never disabled: the focus is on this button, since it was just pressed, and disabled would drop it to the top of the document for a one-second wait. The attribute does not block a second click; the caller refuses the second submit at the source. No effect on a link. |
reason? | string | undefined | Why the button is unavailable. Sets disabled and writes the reason next to it, tied by aria-describedby. Ignored on a link. | |
children | Snippet | The label. A verb, never an icon alone. |
Every other attribute of ButtonOrLinkAttributes passes through to the element.
States
Five variants, three sizes, full width, a link form, unavailable, unavailable with a reason, busy. The link variant is an action written in a sentence: it keeps the button element and takes the underline.
asLink
With href the component renders a link; the text says the destination.
Light
Dark
320 px, light
320 px, dark
216 px, light
Forced colours, captured
busy
aria-busy and aria-disabled, focus kept on the button; the ring is added under motion only.
Light
Dark
320 px, light
320 px, dark
216 px, light
Forced colours, captured
destructive
Light
Dark
320 px, light
320 px, dark
216 px, light
Forced colours, captured
disabled
Unavailable: soft ink and light line, no opacity. The reason is written next to it by the page.
Light
Dark
320 px, light
320 px, dark
216 px, light
Forced colours, captured
fullWidth
Light
Dark
320 px, light
320 px, dark
216 px, light
Forced colours, captured
ghost
Light
Dark
320 px, light
320 px, dark
216 px, light
Forced colours, captured
inSentence
The link variant: an action written in a sentence, underlined, still a button.
Light
Audit Q2 2026, réaudit
Dark
Audit Q2 2026, réaudit
320 px, light
Audit Q2 2026, réaudit
320 px, dark
Audit Q2 2026, réaudit
216 px, light
Audit Q2 2026, réaudit
Forced colours, captured
large
Light
Dark
320 px, light
320 px, dark
216 px, light
Forced colours, captured

primary
Light
Dark
320 px, light
320 px, dark
216 px, light
Forced colours, captured
secondary
Light
Dark
320 px, light
320 px, dark
216 px, light
Forced colours, captured
small
Light
Dark
320 px, light
320 px, dark
216 px, light
Forced colours, captured
withReason
Unavailable with its reason: disabled, and the sentence tied by aria-describedby.
Light
Dark
320 px, light
320 px, dark
216 px, light
Forced colours, captured
Accessibility contract
- Role and name: button or link; the name is the label.
- Keyboard: Tab, Enter, and Space for a button.
- Focus: the ring of the base layer, two pixels of the action colour with an offset.
- Unavailable (
disabled) and busy (aria-busywitharia-disabled) are different states. Busy keeps the focus on the button, because the user just pressed it; the caller refuses the second submit at the source. - Unavailable is never shown with opacity: a translucent wash lowers the contrast of the label. The soft ink and the light line say it, and
reasonsays why.
Content rules
The label is a verb in the infinitive (“Enregistrer”, “Supprimer l’audit”), never “OK”, never an ellipsis. A destructive action says the verb and the object. A reason is a sentence that says what to do first (“Terminez l’audit pour publier le taux.”).
Do and don’t
- Do give the reason on an unavailable button rather than a bare
disabled. - Do not combine
busywith the app’s form busy helper: they would fight over the same attribute. - Do not use the destructive variant as an accent; it borrows the red of non-conformity because it shares the meaning.
- Do use the
linkvariant for an action that sits in a sentence or beside a title (“Renommer”, “Modifier la description”); do not use it for a destination, which is the Link component: the underline is shared, the element is not.
Examples
<Button>Enregistrer</Button>
<Button variant="secondary" href="/lab">Retour aux audits</Button>
<Button variant="destructive">Supprimer l'audit</Button>
<Button busy>Envoi en cours</Button>
<Button reason="Terminez l'audit pour publier le taux.">Publier</Button> Surfaces
- Site: 16 files, plus 37 files still writing the classes by hand.
- App: the auth pages, the settings, the admin screens.
- Extension: to come with the panel’s migration.
Related
Alert, Icon.
Decisions
- 2026-08:
...restpasses the attributes the component does not decide, because a component one works around protects nothing. - 2026-08:
aria-busyabsent rather than"false": an idle button says nothing of its rest. - 2026-09-24: props
variant,size,fullWidth,busyand the newreason; the four variants and the busy ring arerl-buttonrules on the tokens, the base layer keepsbtn-*for the pages that still write them by hand. - 2026-09-26: the
linkvariant, for the app’s five buttons that wore the base layer’s underline class inside a sentence (rename, edit a description or a comment); the app writes no base class any more. - 2026-09-27: the base layer’s
btn-*rules leave with their last raw use, the site’s call to action under a sheet’s criteria, which is this component now;rl-proseexcludes.rl-buttonfrom its link rule instead of matching abtn-name.
Criteria held by construction
- 7.3, Chaque script est-il contrôlable par le clavier et par tout dispositif de pointage ?
A real button or a real link: reachable by Tab, activated by Enter and Space, with no script to reproduce what the element does natively. - 6.1, Chaque lien est-il explicite ?
With href it is a link whose text says the destination; without, a button whose text says the action. - 3.2, Dans chaque page web, le contraste entre la couleur du texte et la couleur de son arrière-plan est-il suffisamment élevé ?
Every variant, the unavailable state and the busy state hold 4.5:1 on their own background (tokens/contrast.json).
Tokens it reads
- Colour:
--rl-action,--rl-action-hover,--rl-bg,--rl-ink,--rl-ink-soft,--rl-line,--rl-line-strong,--rl-nc,--rl-on-fill,--rl-sunken - Typography:
--rl-weight-bold,--rl-text-base,--rl-text-sm,--rl-text-xs - Spacing:
--rl-space-1-5,--rl-space-2,--rl-space-2-5,--rl-space-3,--rl-space-5,--rl-space-6 - Layout:
--rl-radius,--rl-border-strong - Motion:
--rl-duration-fast,--rl-duration-spin,--rl-ease
And one custom property of its own, not tokens: --rl-button-ink.