Skip to content
design system
Thème d’affichage, actuellement Auto

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

  1. The element: a or button, type="button" by default so it never submits a form by accident.
  2. The label, always a verb.
  3. The busy ring, added beside the label when busy is set, under motion only.
  4. With reason, the sentence next to the unavailable button, tied by aria-describedby.

Props

Props of Button
NameTypeDefaultWhat 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?booleanfalseTakes the whole width: narrow forms, panels.
busy?booleanfalseThe 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 | undefinedWhy the button is unavailable. Sets disabled and writes the reason next to it, tied by aria-describedby. Ignored on a link.
childrenSnippetThe 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.

320 px, light
320 px, dark
216 px, light
Forced colours, captured
Button, story asLink, as Chromium renders it with forced colours active

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
Button, story busy, as Chromium renders it with forced colours active

destructive

Light
Dark
320 px, light
320 px, dark
216 px, light
Forced colours, captured
Button, story destructive, as Chromium renders it with forced colours active

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
Button, story disabled, as Chromium renders it with forced colours active

fullWidth

Light
Dark
320 px, light
320 px, dark
216 px, light
Forced colours, captured
Button, story fullWidth, as Chromium renders it with forced colours active

ghost

Light
Dark
320 px, light
320 px, dark
216 px, light
Forced colours, captured
Button, story ghost, as Chromium renders it with forced colours active

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
Button, story inSentence, as Chromium renders it with forced colours active

large

Light
Dark
320 px, light
320 px, dark
216 px, light
Forced colours, captured
Button, story large, as Chromium renders it with forced colours active

primary

Light
Dark
320 px, light
320 px, dark
216 px, light
Forced colours, captured
Button, story primary, as Chromium renders it with forced colours active

secondary

Light
Dark
320 px, light
320 px, dark
216 px, light
Forced colours, captured
Button, story secondary, as Chromium renders it with forced colours active

small

Light
Dark
320 px, light
320 px, dark
216 px, light
Forced colours, captured
Button, story small, as Chromium renders it with forced colours active

withReason

Unavailable with its reason: disabled, and the sentence tied by aria-describedby.

Light
Terminez l'audit pour publier le taux.
Dark
Terminez l'audit pour publier le taux.
320 px, light
Terminez l'audit pour publier le taux.
320 px, dark
Terminez l'audit pour publier le taux.
216 px, light
Terminez l'audit pour publier le taux.
Forced colours, captured
Button, story withReason, as Chromium renders it with forced colours active

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-busy with aria-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 reason says 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 busy with 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 link variant 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: ...rest passes the attributes the component does not decide, because a component one works around protects nothing.
  • 2026-08: aria-busy absent rather than "false": an idle button says nothing of its rest.
  • 2026-09-24: props variant, size, fullWidth, busy and the new reason; the four variants and the busy ring are rl-button rules on the tokens, the base layer keeps btn-* for the pages that still write them by hand.
  • 2026-09-26: the link variant, 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-prose excludes .rl-button from its link rule instead of matching a btn- 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.

Seen in patterns