# @rgaa-dev/design
> The design system of RGAA Lab. One foundation for the web app, the browser extension's side panel and the public site.
> Read packages/design/README.md first, then this inventory, then the component folder you need: Name.svelte is the implementation, Name.md the contract, Name.stories.svelte one story per state, Name.test.ts what is guaranteed.
## Rules
- Tokens are the only source of colour, spacing, type and motion: tokens/*.json, generated by scripts/build.mjs. A literal colour anywhere else fails a test.
- Every documented colour pair is measured at generation and the build fails below its floor (tokens/contrast.json).
- One implementation per component, in Svelte 5, with scoped CSS on tokens and rl- classes; never a Tailwind utility inside a component.
- Colour never carries meaning alone: a status has its word, an icon its label, a disabled control its reason.
- English API and docs; French words for what a user reads. Status labels and symbols come from @rgaa-dev/core.
- Native elements first; every scrolling region focusable and named; a busy control keeps the focus; a live region exists from the first render.
- No em dash, in code, docs or generated files. Comments say why.
## Components
- BusyRegion (atom, stable; app): props busy, as?, class?, children; stories idle, busy, asSection. components/BusyRegion/
- Button (atom, stable; site, app, extension): props variant?, size?, fullWidth?, busy?, reason?, children; stories primary, secondary, ghost, destructive, inSentence, small, large, fullWidth, asLink, busy, disabled, withReason. components/Button/
- Checkbox (atom, stable; no surface yet): props id, label, name?, value?, checked?, hint?, error?, required?, disabled?; stories plain, checked, withHint, required, error, disabled. components/Checkbox/
- Chip (atom, stable; no surface yet): props tone?, icon?, class?, children; stories neutral, info, success, warning, error, withIcon. components/Chip/
- CodeBlock (atom, stable; app, site): props inline?, label?, children; stories inline, block, long. components/CodeBlock/
- DisabledReason (atom, stable; app, extension): props reason, layout?, class?, children; stories iconButton, stacked, group. components/DisabledReason/
- Eyebrow (atom, stable; site, app): props children; stories plain, long. components/Eyebrow/
- Figure (atom, stable; app): props src, alt, width?, height?, href?, caption?, code?, size?; stories evidence, opensLarge, natural. components/Figure/
- GlossaryTerm (atom, stable; extension): props term, open?, children; stories closed, open. components/GlossaryTerm/
- Heading (atom, stable; site, app): props level, size?, measure?, id?, class?, children; stories display, level2, levelWithoutSize, measured. components/Heading/
- Icon (atom, stable; site, app, extension): props name, size?, class?; stories small, medium, large, statuses, all. components/Icon/
- IconButton (atom, stable; no surface yet): props label, name, size?, variant?, pressed?; stories ghost, secondary, small, large, toggle, asLink, disabled. components/IconButton/
- InlineMarkup (atom, stable; site, extension): props text; stories plain, code, emphasis, mixed, codeKeepsMarkers. components/InlineMarkup/
- Kbd (atom, stable; no surface yet): props keys?, children?; stories single, combination, inSentence. components/Kbd/
- LevelBadge (atom, stable; no surface yet): props level, class?; stories a, aa, aaa, inRow. components/LevelBadge/
- Link (atom, stable; site, app, extension): props href, class?, children; stories plain, inSentence. components/Link/
- LiveRegion (atom, stable; no surface yet): props message?, tone?, visible?, class?; stories polite, assertive, hidden. components/LiveRegion/
- Logo (atom, stable; site, app, extension): props height?, markOnly?; stories header, small, large, markOnly. components/Logo/
- ProgressBar (atom, stable; extension): props id, label, value?, max?, showValue?, strip?, class?; stories measured, indeterminate, done, strip. components/ProgressBar/
- Prose (atom, stable; site, app): props size?, measure?, class?, children; stories base, small, large, narrow. components/Prose/
- ScrollRegion (atom, stable; site, app): props label?, labelledBy?, class?, framed?, children; stories labelled, framed, labelledBy. components/ScrollRegion/
- Select (atom, stable; no surface yet): props id, label, name?, value?, options?, groups?, placeholder?, hint?, error?, required?, disabled?; stories plain, chosen, grouped, required, error, disabled. components/Select/
- SkipLink (atom, stable; design, site, app): props href?, children?; stories plain, custom. components/SkipLink/
- StatusChip (atom, stable; site, app): props status, short?; stories c, nc, na, nt, short. components/StatusChip/
- Switch (atom, stable; no surface yet): props id, label, checked?, onchange?, hint?, disabled?; stories plain, on, withHint, disabled. components/Switch/
- Text (atom, stable; app, extension, site): props as?, size?, tone?, bold?, mono?, class?, children; stories plain, soft, tones, inline. components/Text/
- Tooltip (atom, stable; no surface yet): props text, children; stories inSentence, short. components/Tooltip/
- Accordion (molecule, stable; no surface yet): props items, level?, open?, exclusive?, class?, panel; stories independent, exclusive, preset. components/Accordion/
- Alert (molecule, stable; site, app): props tone?, title?, live?, class?, children; stories info, success, warning, error, titled, liveError, liveSuccess. components/Alert/
- AnswerButtons (molecule, stable; extension): props answers, value?, onanswer, labelledBy, class?; stories none, retained, three. components/AnswerButtons/
- AutoVerdict (molecule, stable; extension): props status, source?, findings?, onshow?, class?, children?; stories fromPage, fromEngine, ownSentence, fromAuditor. components/AutoVerdict/
- Banner (molecule, stable; no surface yet): props label, dismissKey, dismissLabel?, children; stories beta, maintenance. components/Banner/
- Breadcrumb (molecule, stable; no surface yet): props items, label?, class?; stories plain, deep. components/Breadcrumb/
- Callout (molecule, stable; site): props rule?, status?, aside?, class?, children; stories strong, subtle, aside, nc, c. components/Callout/
- Card (molecule, stable; site): props href?, listItem?, children; stories plain, link, inList. components/Card/
- CardGrid (molecule, stable; site): props columns?, list?, children; stories three, two, four, list. components/CardGrid/
- CommentField (molecule, stable; extension): props id, label, value?, placeholder?, optional?, hint?, rows?, class?; stories empty, filled, withHint. components/CommentField/
- ConfirmDialog (molecule, stable; no surface yet): props open?, title, confirmLabel, cancelLabel?, destructive?, onconfirm?, oncancel?, children; stories plain, destructive. components/ConfirmDialog/
- ConformityRate (molecule, stable; app): props rate, unavailableReason?, suffix?, decimals?; stories high, middle, low, average, unavailable, unavailableBare. components/ConformityRate/
- CriteriaRecap (molecule, stable; extension): props criteria, current?, reachable?, onjump?, label?, class?; stories plain, reachable. components/CriteriaRecap/
- DataTable (molecule, stable; site): props caption, captionHidden?, columns, rows, rowKey, cell?, empty?; stories plain, withCell, empty, captionHidden. components/DataTable/
- DefinitionList (molecule, stable; no surface yet): props items, columns?, class?, value?; stories plain, rendered, single. components/DefinitionList/
- Dialog (molecule, stable; no surface yet): props open?, title, size?, onclose?, closeLabel?, class?, children, actions?; stories question, form. components/Dialog/
- Disclosure (molecule, stable; no surface yet): props title, level?, open?, ontoggle?, class?, children; stories closed, open, level. components/Disclosure/
- EmptyState (molecule, stable; no surface yet): props title, level?, icon?, class?, children?, action?; stories withAction, search, withIcon. components/EmptyState/
- ErrorSummary (molecule, stable; no surface yet): props errors, title?, level?, class?; stories form. components/ErrorSummary/
- Field (molecule, stable; site, app): props id, label, type?, value?, name?, required?, hint?, error?, autocomplete?, placeholder?, multiline?, rows?, minlength?, maxlength?, inputmode?, autofocus?, code?, children?; stories text, email, required, withHint, error, multiline, code. components/Field/
- FilterGroup (molecule, stable; app): props label, filters, selected?, exclusive?, onchange?, class?; stories exclusive, several. components/FilterGroup/
- JumpList (molecule, stable; extension): props items, current, onjump, summary, open?, class?; stories closed, open. components/JumpList/
- Menu (molecule, stable; no surface yet): props label, items, onselect, children?, align?, class?; stories dots, labelled, withDisabled. components/Menu/
- OTPField (molecule, stable; app): props id, label?, value?, name?, length?, required?, hint?, error?, autofocus?; stories code, seven, error. components/OTPField/
- PageHeader (molecule, stable; no surface yet): props title, eyebrow?, lead?, level?, class?, actions?; stories plain, withAction, section. components/PageHeader/
- Pagination (molecule, stable; no surface yet): props page, pages, label, href, onnavigate?, total?, perPage?, rowsName?, class?; stories short, long, counted. components/Pagination/
- PasswordField (molecule, stable; app): props id, label, value?, name?, required?, hint?, error?, autocomplete?, minlength?, showLabel?; stories signIn, newPassword, error. components/PasswordField/
- Popover (molecule, stable; app, extension): props label, title?, open?, align?, variant?, class?, children; stories help, controls, end. components/Popover/
- PrevNext (molecule, stable; site, app, extension): props prev?, next?, label?, prevWord?, nextWord?, class?; stories both, first, last, buttons. components/PrevNext/
- PropositionList (molecule, stable; extension): props propositions, onaccept, ondismiss, onannounce?; stories mixed, single, none, hooked. components/PropositionList/
- Question (molecule, stable; extension): props criterion, id?, class?, children, help?; stories single, twoCriteria, withHelp. components/Question/
- RadioGroup (molecule, stable; no surface yet): props name, legend, options, value?, hint?, error?, required?, inline?; stories plain, chosen, inline, required, error. components/RadioGroup/
- ResumeBanner (molecule, stable; extension): props summary, alert?, note?, class?; stories resumed, alert. components/ResumeBanner/
- ReviewFlag (molecule, stable; extension): props label, answer?, differences?, onconfirm, class?; stories withDifferences, bare. components/ReviewFlag/
- SearchField (molecule, stable; no surface yet): props id, label, placeholder?, value?, name?, action?, buttonLabel?, onsearch?, clearHref?, clearLabel?; stories inPlace, withClear. components/SearchField/
- Stat (molecule, stable; site): props value, secondaryValue?, label, tone?, children?; stories plain, fraction, withSentence, toned. components/Stat/
- StatGroup (molecule, stable; no surface yet): props items, label, class?; stories audit, two. components/StatGroup/
- StatusBreakdown (molecule, stable; app): props counts, total, label, bar?; stories finished, inProgress, countsOnly. components/StatusBreakdown/
- StatusCounters (molecule, stable; extension): props counts, label?, class?; stories inProgress, finished. components/StatusCounters/
- StatusToggle (molecule, stable; extension): props criterionId, value?, onchange, statuses?, size?, hooks?, class?; stories untested, pressed, twoOnly, medium, hooked. components/StatusToggle/
- Table (molecule, stable; no surface yet): props caption, captionHidden?, variant?, fixed?, class?, children; stories plain, report, captionHidden. components/Table/
- Tabs (molecule, stable; no surface yet): props tabs, selected?, label, class?, panel; stories plain, preset. components/Tabs/
- ThemeSwitch (molecule, stable; site, app, extension): props name?, onchange?; stories plain, named. components/ThemeSwitch/
- Toast (molecule, stable; no surface yet): props messages, ondismiss?, fixed?, passthrough?, dismissLabel?; stories flow, tones, passthrough. components/Toast/
- WizardFooter (molecule, stable; extension): props last?, onprev?, onnext?, onfinish?, prevDisabled?, finishReason?, prevLabel?, nextLabel?, finishLabel?, class?, counters?; stories middle, first, last, blocked. components/WizardFooter/
- CriterionRow (organism, stable; extension): props id, title, status?, onstatus, comment?, oncomment?, engine?, action?, hooks?, class?, children?; stories untested, judged, engine, hooked. components/CriterionRow/
- DangerZone (organism, stable; app): props title, level?, actions, class?, children?; stories single, several, busy. components/DangerZone/
- Derogation (organism, stable; extension): props label, justification, id, onderogate, onremove, class?; stories closed, posed. components/Derogation/
- EvidenceBlock (organism, stable; extension): props criterionId, items, picking?, oncapture, onshow?, onpick?, oncancel?, hooks?, class?, shot?, show?; stories detected, complete, empty, picking, kept. components/EvidenceBlock/
- Onboarding (organism, stable; app): props title, level?, steps, doneWord?, dismissLabel?, ondismiss?, class?, children?, actions?, footer?; stories welcome, underWay, dismissible. components/Onboarding/
- PageSwitcher (organism, stable; extension): props open?, pages, currentId?, onswitch, ondelete?, addPath?, addNote?, onadd?, onclose?, title?; stories pages, single. components/PageSwitcher/
- Section (organism, stable; site): props rhythm?, rule?, class?, children; stories lead, plain, tight, ruled. components/Section/
- ThemeList (organism, stable; extension): props themes, open?, ontoggle?, onstatus, oncomment?, onwizard?, wizardReason?, busyTheme?, criterionAction?, hooks?, level?, class?, detail?; stories themes, offPage, collecting, hooked, partiel. components/ThemeList/
- WizardFrame (organism, stable; extension): props title, kind?, progress?, onclose?, closeLabel?, class?, tools?, above?, children, footer?; stories bare, withProgress, withFooter. components/WizardFrame/
- AppShell (view, stable; app): props home, sections, currentPath, collapsed?, oncollapse?, busy?, announcement?, email, logoutAction, logoutEnhance?, siteHref?, siteLabel?, navLabel?, themeName?, banner?, as?, children; stories open, rail, closed, busy, withBanner. components/AppShell/
- AuthShell (view, stable; app): props title, logoHref?, themeName?, width?, as?, headingLevel?, lead?, children, footer?; stories signIn, message. components/AuthShell/
- PanelShell (view, stable; extension): props label, as?, hidden?, id?, class?, banner?, header?, children, footer?; stories bare, full, underWizard. components/PanelShell/
- PrintPage (view, stable; app, design): props as?, labelledBy?, breakBefore?, keep?, class?, screen?, paper?, children; stories part, kept, plain. components/PrintPage/
- ReportDocument (view, stable; app): props title, subtitle?, coverEyebrow, coverTitle, coverLine?, coverItems, coverFootnote?, printHeader, printHeaderDetail?, plan, summaryExtras?, summaryLabel?, contentsLabel?, rights?, actions?, notice?, children; stories screen, withRights. components/ReportDocument/
## Adding one
Create components/Name/ with the four files, declare an interface Props with a JSDoc line per prop, one exported snippet per state in the stories file, a test that mounts every story, and the doc on the template of an existing one. Then: pnpm --filter @rgaa-dev/design build:tokens (regenerates this file and manifest.json), test.
# The package README
# @rgaa-dev/design
The design system of RGAA Lab. One foundation, two application levels: the web app (`apps/web`) and the extension's side panel (`apps/extension`), plus the public site (`apps/site`) as a third consumer. The reference website is `apps/design`, published at design.rgaa.dev.
This file states the contract. The plan that introduced it is dated 2026-09-24; the sections below say what already holds and what is still being built.
## The five rules
1. **Tokens are the only source of colour, spacing, type and motion.** They live in `tokens/*.json` and are generated into CSS, TypeScript and the Tailwind preset by `scripts/build.mjs`. A literal colour anywhere else fails a test.
2. **Every colour pair is measured, not chosen.** `tokens/contrast.json` lists the pairs and their thresholds; the generator computes the ratios, writes them into the CSS comments and fails the build when one drops below its floor, in light, dark and print.
3. **One implementation per component, in Svelte 5, with scoped CSS.** A component references tokens only, writes `rl-` classes only, and never a Tailwind utility, so the same file renders the same in the app, the panel and the site.
4. **Colour never carries meaning alone.** A status has its word, an icon has its label, a disabled control has its reason. This is criterion 3.1 of the RGAA, and the product audits other sites against it.
5. **English API, French words.** Component names, props, classes and docs are English. Everything a user reads is French. Labels and symbols for audit statuses come from `@rgaa-dev/core`, never from here.
## Layout
```
tokens/ primitives.json, semantic.json, contrast.json the source
scripts/ build.mjs (every generator, --check), lib/ (one module per output)
styles/ tokens.css, tokens.scoped.css (generated), base.css, print.css
fonts/ fonts.css, LICENSE-OFL.txt
theme/ theme.ts, boot.js
icons/ svg/ (the source drawings), paths.ts (generated), index.ts (svgIcon and statusIcon for markup written as a string)
components/ one folder per component: Name.svelte, Name.md, Name.stories.svelte, Name.test.ts
tooling/ rehype-tables.js, the mdsvex plugin for Markdown tables (scope and caption)
manifest.json generated inventory, schema in manifest.schema.json
llms.txt generated summary for LLM contributors
```
Today (0.2.0) the tokens, the theme, the fonts, the base layer, the component folders and the generated inventory are in place, and the 85 components (the 18 original ones rewritten, plus 67 written on the target shape: Icon, Link, BusyRegion, Dialog, ConfirmDialog, LiveRegion, SkipLink, Kbd, Disclosure, Accordion, Tabs, ErrorSummary, Checkbox, RadioGroup, Switch, Select, IconButton, Breadcrumb, Chip, LevelBadge, EmptyState, Table, Menu, Toast, Pagination, ProgressBar, Tooltip, SearchField, Banner, StatGroup, PageHeader, DefinitionList, AuthShell, AppShell, StatusBreakdown, CodeBlock, Figure, ReportDocument, DisabledReason, Text, PasswordField, OTPField, FilterGroup, Popover, PrevNext, PrintPage, Onboarding, DangerZone, the wizard skeleton of the extension's panel: WizardFrame, Question, AnswerButtons, CommentField, WizardFooter, CriteriaRecap, StatusCounters, AutoVerdict, Derogation, JumpList, ResumeBanner, ReviewFlag, GlossaryTerm, and the panel's own screens: PanelShell, ThemeList, CriterionRow, StatusToggle, EvidenceBlock, PageSwitcher) are on the English API with scoped CSS on the tokens, `status: stable`. The base layer's `btn-*`, `card`, `chip`, `tableau*`, `h-display` and `link-underline` rules left with their last raw use; it keeps the document defaults, the focus ring, `rl-sr-only`, the page column `rl-container` (and `--wide`), `rl-prose` and the motion switch. The changelog says where things stand.
## Consuming it
- App and site: in `app.css`, import in this order `@rgaa-dev/design/fonts.css`, `@rgaa-dev/design/tokens.css`, `tailwindcss/base`, `@rgaa-dev/design/base.css`, `tailwindcss/components`, `tailwindcss/utilities` (a test holds the order); use the Tailwind preset for page layout; import components from `@rgaa-dev/design/components`. Apply the theme before first paint with the code of `theme/boot.js` inline in `app.html`, and read or change it through `@rgaa-dev/design/theme`.
- Extension panel: the same fonts, tokens and base, bundled by esbuild from `sidepanel.css`; `theme/boot.js` built to a file and loaded before the stylesheets; the same components, compiled by the in-repo plugin.
- Extension overlays injected into audited pages: `tokens.scoped.css` on a host element carrying `class="rl-scope"` and `data-theme`, never on the page's `:root`.
- Documents the product exports (reports, declarations, emails): the generated token modules, never a pasted copy.
- A surface that prints a document (the app for its report, the design site for its print pattern) imports `@rgaa-dev/design/print.css` after `base.css`: the page's size and margins, the breaks, the headings kept with their text, the address after a link. `ReportDocument` and `PrintPage` rely on it; the colours on paper are the token sheet's print set, loaded with the tokens.
- The site's Markdown pipeline takes `@rgaa-dev/design/tooling/rehype-tables.js` in its mdsvex `rehypePlugins`, for `scope="col"` and the `Tableau :` caption on every table.
## Adding or changing a component
1. Pick the level (atom, molecule, organism, view) and an English, singular, PascalCase name.
2. Create `components/Name/` with the four files. Nothing else goes in the folder.
3. `Name.svelte`: a header comment that says why the component exists, an `interface Props` with a JSDoc line on every prop, `rl-` classes, tokens only.
4. `Name.stories.svelte`: one exported snippet per state that changes markup or ARIA. A story that writes an id or a group name takes a `scope` argument and suffixes it (`id="story-nom{scope}"`): the design site shows every story several times on one page, and the test harness passes none.
5. `Name.md`: the documented sections, in order, with the RGAA criteria the component satisfies and why.
6. `Name.test.ts`: mount every story and assert the accessibility contract (name, role, keyboard, focus).
7. `pnpm --filter @rgaa-dev/design build:tokens` (it regenerates `manifest.json` and `llms.txt` too), then `test`, then look at the page on the design site in all four conditions.
8. `pnpm --filter @rgaa-dev/design-site build`, then `captures`: the forced-colours picture of every story is committed under `apps/design/static/captures/`, and a test of the site refuses a capture older than its sources. `run audit` (the bare `audit` is pnpm's own command) and `keyboard` run the engine and a Tab walk over every page of the site.
9. A changelog line. If the component retires a hand-built pattern somewhere, list it under `replaces` and remove the copy in the same branch.
## Commands
```
pnpm --filter @rgaa-dev/design build:tokens regenerate from tokens/*.json and the component folders (fails on a contrast breach)
pnpm --filter @rgaa-dev/design check:generated what CI runs: the committed files match the source
pnpm --filter @rgaa-dev/design test
pnpm --filter @rgaa-dev/design typecheck svelte-check, so the stories' module exports are typed
```
## Reading it as a machine
`manifest.json` lists every component with its level, status, surfaces, props (type, default, description read from the `Props` interface), snippets, stories, `rl-` classes and criteria; `manifest.schema.json` is its schema. `llms.txt` is the same inventory as a page of text, rules first. Both are generated; a doc that breaks the section order, claims a criterion the referential does not have, or annotates a story that does not exist fails `docs.test.ts` before the generator runs.
# BusyRegion
---
name: BusyRegion
slug: busy-region
level: atom
status: stable
surfaces: [app]
since: 2026-09
replaces:
- "apps/web: the zone-occupee class and aria-busy written by hand on the lab layout's main"
criteria:
- code: "7.1"
why: "The wait is a state assistive technology can read: aria-busy on the zone, set and cleared by the page, never a spinner alone."
- code: "13.8"
why: "The ring turns under motion only; with reduced motion the hollow and the rule at the top say the wait."
patterns: [app-shell]
related: [button]
---
## Purpose
The busy state at the scale of a page: while a navigation loads, the content area drops to the hollow, takes a rule at the top and a fixed ring at the corner, and says so with `aria-busy`. The class and the attribute go together, so the rule cannot repaint everything that already carries `aria-busy`.
## When to use, when not
Use it on the content area of an app shell, driven by the navigation state, or on a part of a page that reloads on its own. Do not use it for a button (Button has `busy`) nor for a wait shorter than a frame.
## Anatomy
1. A `main`, `section` or `div`, with `aria-busy` when busy.
2. Under motion, a ring fixed at the top right of the viewport, in the soft ink.
## States
Idle and busy.
## Accessibility contract
- Role and name: those of the element chosen with `as`; the component adds none.
- Keyboard, focus: none. Busy never moves the focus.
- Live regions: none; `aria-busy` is the state, and the page announces the outcome when the wait ends.
- Reduced motion: no ring; the hollow and the rule carry the wait.
## Content rules
None of its own.
## Do and don't
- Do drive `busy` from the navigation state, so it clears by itself.
- Do not set `aria-busy` by hand next to it.
## Examples
```svelte
{@render children()}
```
## Surfaces
- App: the lab layout's content area, through the AppShell.
## Related
Button.
## Decisions
- 2026-09-24: created from the base layer's `zone-occupee`, which stayed for the lab layout until the shell moved to the atoms on 2026-09-25.
# Button
---
name: Button
slug: button
level: atom
status: stable
surfaces: [site, app, extension]
since: 2026-08
replaces:
- "apps/web: raw btn-* classes on twelve pages, three ways of writing Cancel, three ways of full width"
- "apps/extension: a11y-btn-primary, a11y-verdict-btn, a11y-export-modal-btn, a11y-answer-btn"
criteria:
- code: "7.3"
why: "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."
- code: "6.1"
why: "With href it is a link whose text says the destination; without, a button whose text says the action."
- code: "3.2"
why: "Every variant, the unavailable state and the busy state hold 4.5:1 on their own background (tokens/contrast.json)."
patterns: [destructive-confirmation, disabled-with-reason, form-with-error-summary]
related: [alert, icon]
---
## 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`.
## 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.
## 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
```svelte
```
## 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.
# Checkbox
---
name: Checkbox
slug: checkbox
level: atom
status: stable
surfaces: []
since: 2026-09
replaces:
- "apps/web: the hand-built checkboxes of the settings, the sharing and the admin forms, once the app is ported"
criteria:
- code: "11.1"
why: "The label's for and the control's id come from one prop, so a checkbox cannot be rendered without its label."
- code: "11.10"
why: "The error is tied by aria-describedby and aria-invalid together, and read as an alert without moving the focus."
- code: "3.1"
why: "The native mark says the checked state as a shape; accent-color only tunes the colour."
patterns: [form-with-error-summary]
related: [radio-group, switch, field]
---
## Purpose
A checkbox with its label, native. The browser gives the focus, the Space key, the state to assistive technology and a mark that says it without colour; the component ties the label, the hint and the error so none can be left aside.
## When to use, when not
Use it for a yes-or-no choice inside a form, submitted with it, and for a list of independent options. Not for a setting that applies at once (Switch), not for an exclusive choice (RadioGroup).
## Anatomy
1. The control, native, sized and tuned by `accent-color`.
2. The label, with the required mark when needed.
3. The hint and the error, under, tied by `aria-describedby`.
## States
Plain, checked, with a hint, required, in error, and unavailable.
## Accessibility contract
- Role and name: a checkbox, named by its label; "(obligatoire)" in the name when required.
- Keyboard, focus: Tab reaches it, Space toggles; the ring is the system's.
- Live regions: the error is `role="alert"`, so a correction is announced without moving the focus.
## Content rules
French. The label says what checking means, as a statement ("J'accepte que le rapport soit publié") or a noun ("Formulaires"). The error says what to do ("Cochez la case pour publier le rapport.").
## Do and don't
- Do keep several checkboxes of one question in a `fieldset` with a `legend`; RadioGroup shows the shape.
- Do not redraw the control with `display: none` on the input.
- Do not use a checkbox for a setting that applies at once; that is Switch.
## Examples
```svelte
```
## Surfaces
- App: the settings, the sharing options, the admin forms; not ported yet.
## Related
RadioGroup, Switch, Field.
## Decisions
- 2026-09-24: native control, `accent-color`, as the site's cases-à-cocher demo argues; redrawing would cost the mark and the system's focus ring for nothing.
# Chip
---
name: Chip
slug: chip
level: atom
status: stable
surfaces: []
since: 2026-09
replaces:
- "packages/design base.css: the chip class, and its raw uses in the app, once the app is ported"
criteria:
- code: "3.1"
why: "The word is required and carries the information; the tone is an ink on a line that only confirms it, and a drawing is hidden."
related: [status-chip, level-badge]
---
## Purpose
A short word in a frame: a state, a category, a count. The word carries the information and the tone only confirms it. Audit statuses are not chips: StatusChip has its own drawing and its words from core.
## When to use, when not
Use it next to a title or in a table cell for a state ("Brouillon", "Terminé"), a category or a small count. Not for an audit status (StatusChip), not for a criterion level (LevelBadge), not as a button or a link.
## Anatomy
1. A `span` with the line and, for a tone, the ink.
2. An optional drawing, hidden.
3. The word.
## States
Neutral, the four tones, and with a drawing.
## Accessibility contract
- Role and name: none; it is text in the flow, read as its word.
- Keyboard, focus: none.
- Live regions: none.
## Content rules
French. One or two words, a noun or a participle ("Terminé", "En attente"); never a sentence.
## Do and don't
- Do choose the tone from the meaning: a warning tone for a decoration is a lie.
- Do not put a chip inside a button's label; the button's text is its name.
## Examples
```svelte
TerminéBrouillon
```
## Surfaces
- App: the audit's state and closure, the site's audit list, the members' roles, the users' state and second factor, an expired token.
## Related
StatusChip, LevelBadge.
## Decisions
- 2026-09-24: the same tones as Alert, so a state has one ink whether it is a message or a chip.
# CodeBlock
---
name: CodeBlock
slug: code-block
level: atom
status: stable
surfaces: [app, site]
since: 2026-09
replaces:
- "apps/web: the code elements and mono spans written by hand for selectors, secrets and tokens"
criteria:
- code: "10.11"
why: "A long value wraps anywhere, so a token or an address never makes the page scroll sideways at 320 pixels."
- code: "10.4"
why: "The size is a token of the scale, so the text grows with the reader's zoom like the rest."
patterns: []
related: [kbd, figure]
---
## Purpose
Text the reader copies rather than reads: a selector, a secret key, a token, a line of code. In the mono face, on the sunken surface, inline in the running text or as a block.
## When to use, when not
Use it for a value that is code or behaves like it: a CSS selector under a comment, a secret to type into an authenticator, a masked token. Not for a key of the keyboard (Kbd), not for an identifier that reads as a word, such as a criterion's number.
## Anatomy
1. Inline: a `code` element.
2. Block: a `pre` holding a `code`; with `label`, the `pre` is a `group` named by it, since a bare `pre` is generic and cannot take a name.
## States
Inline in a sentence, a block, and a long value that wraps.
## Accessibility contract
- Role and name: `code` inline, read as code by the readers that say so; a block is a `pre`, which becomes a `group` named by `label` when one is given, and stays generic and nameless otherwise.
- Keyboard, focus: none; a block that overflows does not scroll, it wraps.
- Live regions: none.
## Content rules
The text is the value, verbatim, nothing added. The label, when there is one, says what the value is ("Clé de secours"), in French.
## Do and don't
- Do give a block a label when its content is not obvious from the sentence before it.
- Do not put a sentence in it: it is for values, and its face says so.
## Examples
```svelte
Le sélecteur footer .legal-links désigne l'élément.
JBSW Y3DP EHPK 3PXP
```
## Surfaces
- App: the selectors of the audited page and the report, the second factor's key, the temporary password, the masked tokens.
- Site: the sheets' code samples, when they leave the hand-written `pre`.
## Related
Kbd, Figure.
## Decisions
- 2026-09-26: no copy button: the product's values are short and selected in one gesture, and a button per value is a stop per value at the keyboard.
# DisabledReason
---
name: DisabledReason
slug: disabled-reason
level: atom
status: stable
surfaces: [app, extension]
since: 2026-09
replaces:
- "apps/extension: the title attribute on the theme rows' disabled wizard button ('Revenez à la page auditée')"
criteria:
- code: "11.10"
why: "The reason is tied to the control by aria-describedby: a reader hears why the control refuses, next to its name, and not only a disabled state."
- code: "3.1"
why: "The refusal is a sentence, not a greyed look: the soft ink of the control only confirms what the words say."
- code: "10.11"
why: "The sentence wraps under the control on a narrow screen; nothing is cut."
patterns: [disabled-with-reason]
related: [button, icon-button]
---
## Purpose
A control that refuses, and says why next to it. The component writes the reason as a visible sentence and hands the control the two attributes that make the refusal honest: `disabled`, and `aria-describedby` pointing at the sentence. `Button` carries the same contract as its own `reason` prop; this component is for every other control, and for a group that shares one reason.
## When to use, when not
Use it around an icon button, a switch, a field or a group of controls that cannot be used until a condition is met: off the audited page, a report already published, an audit not finished. Use `Button`'s `reason` prop for a lone button. Do not use it for a control that is busy (that is `busy`, which keeps the focus), and do not use it to hide a control the user can never use: remove that one.
## Anatomy
1. The wrapper, the reason beside or under the control.
2. The control, rendered by the snippet with the marks spread on it.
3. The reason, a sentence in the soft ink, with the id the control describes itself by.
## States
An icon button, a full-width control with the reason under it, and a group sharing one reason.
## Accessibility contract
- Role and name: the control's own; the sentence is its description.
- Keyboard, focus: a disabled control leaves the tab order, as the platform does; the reason stays readable by the virtual cursor and in the description.
- Live regions: none. A reason that changes is read the next time the control is reached; a page that wants it announced at once writes it in a LiveRegion too.
## Content rules
French. The reason says what to do first, in the imperative: "Terminez l'audit pour publier.", "Revenez à la page auditée pour capturer.". Never "Indisponible" alone, never the negative fact ("Rapport non relu").
## Do and don't
- Do spread the marks on every control of a group; the sentence is written once.
- Do not write the reason in a `title`: a disabled element gets no hover, and a tooltip needs a pointer.
- Do not grey the control with opacity; the marks and the tokens' disabled rules do the drawing.
## Examples
```svelte
{#snippet children(marks)}{/snippet}
```
## Surfaces
- App: the audit settings once the report is published, the export buttons of an unfinished audit.
- Extension: the wizard buttons of the theme rows while the tab is off the audited page.
## Related
Button, IconButton.
## Decisions
- 2026-09-26: the snippet receives the marks rather than the component wrapping the control in a fieldset: a `fieldset disabled` would work for native controls only, and would put a group role around one button.
- 2026-09-26: `Button` keeps its own `reason` prop rather than composing this component: nine of ten reasons in the product sit on a button, and one prop reads better than a snippet.
# Eyebrow
---
name: Eyebrow
slug: eyebrow
level: atom
status: stable
surfaces: [site, app]
since: 2026-08
criteria:
- code: "9.1"
why: "Rendered as a paragraph, never as a heading: it is not a level of the outline, and a screen reader must not count it as one."
patterns: []
related: [heading, prose, section]
---
## Purpose
The small line above a title that situates the page at a glance: the version of the standard, the scope, the browsers. It is never decorative, so what it says has to be true and useful.
## When to use, when not
Use it above the first heading of a page or a section, for one fact the reader needs before the title. Do not use it as a label, a badge or a second title, and never for text that carries the argument: that is the heading's job.
## Anatomy
1. A paragraph in small capitals, soft ink, wide tracking.
## States
One state: the eyebrow has no interaction and no variant.
## Accessibility contract
- Role and name: a paragraph. It is read in the flow, before the heading it introduces.
- Keyboard: none.
- Focus: none.
- Live regions: none.
## Content rules
Short, factual, French. Middle dots separate items ("RGAA 4.1.2 · Chrome et Firefox · Gratuit"). No sentence, no verb.
## Do and don't
- Do put one fact per item, and only facts the reader can verify.
- Do not turn it into a heading because it looks like one: the outline is the heading's, criterion 9.1.
## Examples
```svelte
RGAA 4.1.2 · Chrome et Firefox · GratuitLes 106 critères.
```
## Surfaces
- Site: 34 pages open with it.
- App: the error page and the contents heading of the report.
## Related
Heading, Prose, Section.
## Decisions
- 2026-08: a paragraph rather than a heading, so it never enters the outline.
- 2026-09-24: an `rl-eyebrow` rule on the tokens, the eyebrow tracking the tokens already defined for it.
# Figure
---
name: Figure
slug: figure
level: atom
status: stable
surfaces: [app]
since: 2026-09
replaces:
- "apps/web: the figure written by hand on the audited page and in the report for the screenshots of a non-conformity"
criteria:
- code: "1.1"
why: "The picture carries an alt that says what it proves; the caption says where."
- code: "13.2"
why: "When the picture opens large in a new window, its name says so."
- code: "10.11"
why: "The picture never exceeds its frame, and a caption that is a selector wraps anywhere."
patterns: []
related: [code-block]
---
## Purpose
A picture with its caption. In the product it is evidence: the element found non-compliant, photographed by the extension at the time of the audit, with the selector under it.
## When to use, when not
Use it for a picture that proves or shows something the text cannot: a screenshot of an element, a diagram. Not for decoration, which the direction refuses, and not for the mark, which is Logo.
## Anatomy
1. The picture, with its intrinsic size, lazy.
2. A link around it when it opens large, named for it.
3. The caption, plain or in the mono face.
## States
Evidence with its selector, a picture that opens large, and a picture at its natural size.
## Accessibility contract
- Role and name: `figure` with a `figcaption`; the `img` has the alt given, extended with "(ouvrir en grand, nouvelle fenêtre)" when it is a link.
- Keyboard, focus: the link, when there is one, with the base layer's ring.
- Live regions: none.
## Content rules
French. The alt is a sentence that names the element and the criterion ("Élément footer .legal-links, en défaut sur le critère 3.2"). The caption is the selector, or the source of a diagram.
## Do and don't
- Do give the intrinsic width and height, so the page does not jump.
- Do write the alt as what the picture proves, not "capture d'écran".
- Do not use it for a picture that says nothing the text does not: the direction forbids illustration.
## Examples
```svelte
```
## Surfaces
- App: the screenshots of a non-conformity on the audited page and in the report.
## Related
CodeBlock.
## Decisions
- 2026-09-26: the whole picture is the link rather than a "voir en grand" link under it, since the picture is what one wants larger, and one target beats two.
# GlossaryTerm
---
name: GlossaryTerm
slug: glossary-term
level: atom
status: stable
surfaces: [extension]
since: 2026-09
replaces:
- "apps/extension: the .a11y-terme buttons the wizards wrote and attacherLesTermes wired"
criteria:
- code: "7.1"
why: "A button with aria-expanded and aria-controls, operable at the keyboard, where a tooltip on hover would be reached by nobody."
- code: "10.13"
why: "The definition appears on a press and stays until pressed again; it is not content shown on hover or focus that vanishes."
related: [disclosure, question, tooltip]
---
## Purpose
A term of the referential inside a wizard's sentence, with its definition a press away, in the flow.
## When to use, when not
Use it for a word of the referential the auditor may not know ("alternative textuelle", "contexte du lien"). Not for a help sentence about the whole question, which is the Question's help line, and not for a tooltip on a control.
## Anatomy
1. The term, a button with a dotted line under it.
2. The definition, a block under the sentence, hidden until pressed.
## States
Closed, and open.
## Accessibility contract
- Role and name: a `button` named by the term with "(définition)" hidden after it; `aria-expanded` says the state and `aria-controls` names the block.
- Keyboard, focus: Tab reaches it, Enter or Space toggles; the focus stays on the button.
- Live regions: none; the block opens in the flow, under the sentence.
## Content rules
French; the term as the referential's glossary writes it, the definition quoted from it or one sentence of the product's.
## Do and don't
- Do keep the definition short; the glossary entry on the site is the long version.
- Do not turn the term into a link: leaving the panel loses the audit in progress.
## Examples
```svelte
L'Le texte associé à une image, restitué à sa place par un lecteur d'écran. décrit-elle l'image ?
```
## Surfaces
- Extension: the wizard questions.
## Related
Disclosure, Question, Tooltip.
## Decisions
- 2026-09-26: a button and a block in the flow rather than a tooltip: the tool cannot commit the fault it audits, and a block that stays is read at the reader's pace.
# Heading
---
name: Heading
slug: heading
level: atom
status: stable
surfaces: [site, app]
since: 2026-08
criteria:
- code: "9.1"
why: "The level is a required prop, separate from the size: the outline of the page is decided by the document, never by how big a title should look."
patterns: []
related: [eyebrow, prose, section]
---
## Purpose
A heading whose level and size are two different things. Choosing a tag "for the size that looks right" is exactly the fault criterion 9.1 sanctions; here the level is required and the size is a separate parameter on the type scale.
## When to use, when not
Use it for every heading of a page: the thesis (display), the sections, the sub-sections. Do not use it for a bold line that is not a heading, and do not skip a level to get a smaller size: change `size` instead.
## Anatomy
1. An `h1` to `h4`, in the display typography (weight 700, tight tracking, balanced wrapping).
2. Optionally a measure in characters.
## States
Four levels, six sizes, an optional measure. No interaction.
## Accessibility contract
- Role and name: the heading of its level; the name is its text.
- Keyboard, focus, live regions: none. Screen readers navigate by heading level, which is why the level must be true.
- One `h1` per page; levels never skip. Site and app tests check both.
## Content rules
French, no trailing period on a short heading, a period when the heading is a sentence ("Les 106 critères."). Say the thing, not the section's name.
## Do and don't
- Do pick the level from the outline and then the size from the place.
- Do not use `size` to fake a hierarchy the outline does not have.
- Do not render two `h1`.
## Examples
```svelte
Les composants, dans tous leurs états.ActionsParamètres du rapport
```
## Surfaces
- Site: every page (38 files).
- App: the auth pages, the settings, the admin screens, the audit pages.
## Related
Eyebrow, Prose, Section.
## Decisions
- 2026-08: `level` is required and has no default, so a heading cannot be added without deciding where it sits.
- 2026-09-24: the display weight is written as 700; the font has no 750 and every browser rendered 700.
- 2026-09-24: `rl-heading` rules on the display tokens and the type scale; the base layer keeps `h-display` for the article layout until it moves to the atoms.
- 2026-09-27: `h-display` leaves the base layer, with no raw use left; `rl-prose` carries the display face for a Markdown body's headings.
- 2026-09-26: an `id` prop, since a section that names itself by its heading needs one, and a `base` size for the fourth level of the report, a criterion inside a theme inside a part.
# Icon
---
name: Icon
slug: icon
level: atom
status: stable
surfaces: [site, app, extension]
since: 2026-09
replaces:
- "apps/web: the inline SVGs of the lab layout, one per navigation entry"
- "apps/extension: the SVG strings of the panel header and the wizards"
criteria:
- code: "1.2"
why: "The svg is always aria-hidden and focusable=false; assistive technology skips it and reads the word next to it."
patterns: []
related: [status-chip, alert, button]
---
## Purpose
A drawing that goes with a word. Twenty-three stroke icons on a 24-unit grid, drawn in the current colour, so they follow the ink in both themes and under forced colours. The icon is never the message: the word is, and the icon is hidden from assistive technology whatever the caller does.
## When to use, when not
Use it next to a label in a control, a chip, a heading or a navigation entry, when the shape helps a sighted reader find it faster. Do not use it alone, and do not use it where a character does the job: an audit status in running text or a document takes the symbol from `@rgaa-dev/core`.
## Anatomy
1. An `svg` with `aria-hidden="true"` and `focusable="false"`.
2. One or more `path` elements from `icons/paths.ts`, generated from `icons/svg`.
## States
The three sizes, the five status drawings with their words, and the whole set.
## Accessibility contract
- Role and name: none. The svg is hidden; the parent's text is the name.
- Keyboard and focus: none; `focusable="false"` keeps old Edge from tabbing to it.
- Colour: `currentColor`, so the icon has exactly the contrast of its text.
- Forced colours: a stroke in `currentColor` is repainted by the system palette like text.
## Content rules
The word next to the icon says what the icon means, in French, visible or in `rl-sr-only`. A status icon comes with the status label from `@rgaa-dev/core`.
## Do and don't
- Do pick a status drawing through `statusIcon`, never by shape.
- Do not give the svg a title or a label: two names for one thing is one too many.
## Examples
```svelte
Conforme
Non conforme
```
For an overlay written without Svelte, `svgIcon('check', { size: 'sm' })` from `@rgaa-dev/design/icons` returns the same markup as a string.
## Surfaces
- Site: the header and the sheet navigation, as they move to the atoms.
- App: the lab layout, as it moves to AppShell.
- Extension: the panel and the page furniture, through `svgIcon`.
## Related
StatusChip, Alert, Button.
## Decisions
- 2026-09-24: `cross` and `close` are the same drawing under two names, because they are two meanings: an audit verdict and a control that dismisses. A reader of the call site should know which one is meant.
- 2026-09-24: the eight navigation drawings (menu, chevron-left, home, document, building, settings, shield-check, logout) are the lab layout's, moved into the set as they were, so the rail does not change shape.
- 2026-09-24: no fill, no colour, only paths: a test on the source files refuses anything else, so an icon can be pasted from any set as long as it is redrawn on the grid.
# IconButton
---
name: IconButton
slug: icon-button
level: atom
status: stable
surfaces: []
since: 2026-09
replaces:
- "apps/web: the close, menu and rail buttons of the lab layout and the row menus, once the app is ported"
- "apps/extension: the header and toolbar buttons of the panel, when it is on Svelte"
criteria:
- code: "11.9"
why: "The name is a required prop written to aria-label, so a button that shows only a drawing still says what it does."
- code: "7.1"
why: "A toggle writes aria-pressed from its state at every render; a button that is not one carries no state."
- code: "3.1"
why: "The pressed state is told by the fill and the line, and under forced colours by the system's highlight."
patterns: [app-shell]
related: [button, icon]
---
## Purpose
A button that shows a drawing and says a word. The product's toolbars are full of them; an icon alone is a button with no name, the fault the product finds every day on other sites. Here the name is required, the drawing is hidden, and a toggle says its state.
## When to use, when not
Use it in a toolbar, a table row or a header where the space is a square: close, menu, previous, next, settings. Do not use it for a page's action (Button, with a verb), and never with an icon whose meaning is not universal: "Enregistrer" is a word, not a floppy disk.
## Anatomy
1. A square button or link, the size of a control.
2. The drawing, from the icon set, hidden.
3. The name, in `aria-label`.
## States
Ghost, secondary, small, large, a toggle, a link, and unavailable.
## Accessibility contract
- Role and name: a button or a link, named by `label`. A toggle carries `aria-pressed`.
- Keyboard, focus: the browser's; the ring is the system's.
- Live regions: none; a toggle announces its own state.
- Pointer: the whole square is the target, at least the control size, above the pointer target floor.
## Content rules
French. The name is what the button does, a verb or a noun the user knows ("Fermer", "Menu", "Page suivante", "Paramètres"), never the icon's name.
## Do and don't
- Do write the name as the user would say it, not as the drawing is called.
- Do use `pressed` only for a toggle, and leave it absent otherwise.
- Do not rely on a `title` tooltip for the name.
## Examples
```svelte
```
## Surfaces
- App: the lab layout's rail and drawer, the row menus; not ported yet.
- Extension: the panel header and the wizard toolbar, when it is on Svelte.
## Related
Button, Icon.
## Decisions
- 2026-09-24: `label` is required and not defaulted from the icon's name: the icon's name is not what the user needs to hear.
# InlineMarkup
---
name: InlineMarkup
slug: inline-markup
level: atom
status: stable
surfaces: [site, extension]
since: 2026-08
criteria:
- code: "10.7"
why: "Rendered through real elements, never an HTML string, so a criterion sheet cannot inject markup even by accident."
patterns: []
related: [prose]
---
## Purpose
Text with two enrichments and two only: backticks give `code`, double asterisks give `strong`, single ones give `em`. The criteria sheets are typed data, not Markdown, and the extension must render them offline without a parser; but accessibility documentation without code fragments is unreadable, since `alt=""` and no `alt` do not mean the same thing.
## When to use, when not
Use it for a sentence from a criterion sheet or a glossary entry. Do not use it for a paragraph that needs links, headings or lists: that text should be split into fields.
## Anatomy
1. Segments of plain text.
2. `code` spans on the hollow background.
3. `strong` and `em` spans.
## States
Plain text, with code, with emphasis, mixed.
## Accessibility contract
- Role and name: inline text; `code`, `strong` and `em` are semantic elements a screen reader can announce.
- No HTML string is ever built: the text is split into segments and rendered by real elements.
## Content rules
The two markers do not nest: inside a code span a double asterisk stays literal, which is what a code fragment needs.
## Do and don't
- Do keep a sheet's sentence to these two enrichments.
- Do not add links or lists here; split the sheet instead.
## Examples
```svelte
```
## Surfaces
- Site: the criteria sheets.
- Extension: the criterion documentation in the panel, once on Svelte.
## Related
Prose.
## Decisions
- 2026-08: italic was first omitted and eight passages showed bare asterisks; a format four authors get wrong is not an authors' problem, so it is accepted and a test flags any marker left unrendered.
- 2026-09-24: renamed from Riche, prop `text`; the code span is a scoped rule on the tokens.
# Kbd
---
name: Kbd
slug: kbd
level: atom
status: stable
surfaces: []
since: 2026-09
criteria:
- code: "8.9"
why: "The kbd element is used for what it means, a key the user presses, and never for its look; a key drawn with a span would be presentation only."
- code: "12.10"
why: "Where a single-key shortcut exists, the component is how the page says it, so the shortcut is documented next to the control it drives."
related: [inline-markup]
---
## Purpose
A key, or a combination of keys, as the user presses them. The product explains keyboard use everywhere: the extension's wizard shortcuts, the site's sheets, the app's help. A key drawn as a key is read as one, and the `kbd` element says so to assistive technology.
## When to use, when not
Use it for a key or a shortcut in running text or in a help table. Not for a button's label, not for code (that is InlineMarkup's code span).
## Anatomy
1. A `kbd` for one key; for a combination, an outer `kbd` holding one `kbd` per key, joined by a plus.
## States
One key, a combination, and inside a sentence.
## Accessibility contract
- Role and name: the `kbd` element; a screen reader reads the key's name. The plus between keys is read too, "Ctrl plus K", which is how the combination is said aloud.
- Keyboard, focus: none, it is text.
- Live regions: none.
## Content rules
French key names as the keyboards of the product's users write them: Échap, Entrée, Maj, Ctrl, Tab, Espace; arrows as "Flèche droite". Not the English names.
## Do and don't
- Do write the key as it is printed on the key cap.
- Do not use it to style a button or a code sample.
## Examples
```svelte
Appuyez sur Tab pour passer au champ suivant, pour revenir.
```
## Surfaces
- Extension: the wizard shortcut help, when the panel is on Svelte.
- Site: the sheets that name keys.
## Related
InlineMarkup.
## Decisions
- 2026-09-24: nested `kbd` for a combination, as the HTML standard describes, rather than one element with a plus in its text.
# LevelBadge
---
name: LevelBadge
slug: level-badge
level: atom
status: stable
surfaces: []
since: 2026-09
replaces:
- "apps/web: the level letters written raw next to the criteria of the audit and the report, once the app is ported"
criteria:
- code: "3.1"
why: "The level is a code, not a colour: the three tiers share one drawing and the word says which."
related: [chip, status-chip]
---
## Purpose
The level of a criterion, A, AA or AAA, as three letters in a mono frame, read as "niveau AA". The word is hidden before the code so a list of criteria read aloud says what the badge means.
## When to use, when not
Use it next to a criterion wherever the level matters: the audit page, the report, the criteria list. Not for anything that is not a level of the referential.
## Anatomy
1. A `span` in the mono face with the strong line.
2. The hidden word "Niveau".
3. The level.
## States
A, AA, AAA, and in a criterion row.
## Accessibility contract
- Role and name: none; text in the flow, read "Niveau AA".
- Keyboard, focus: none.
- Live regions: none.
## Content rules
The level as the referential writes it, nothing else.
## Do and don't
- Do not colour the levels: they are tiers of the standard, not degrees of gravity.
## Examples
```svelte
```
## Surfaces
- App: the audit page and the report; not ported yet.
- Extension: the criterion rows, when the panel is on Svelte.
## Related
Chip, StatusChip.
## Decisions
- 2026-09-24: the hidden word is "Niveau", as the referential names it; a badge reading "A A" aloud was the fault on the audit page.
# Link
---
name: Link
slug: link
level: atom
status: stable
surfaces: [site, app, extension]
since: 2026-09
replaces:
- "apps/site and apps/web: the link-underline class written by hand in fifty files"
criteria:
- code: "3.1"
why: "The underline carries the link and the hover thickens it; the colour only supports both, so the link reads without colour."
- code: "6.1"
why: "The text is the name: the component adds nothing to it, so the caller has to write a text that says the destination."
patterns: []
related: [button]
---
## Purpose
A link in running text, underlined. The underline carries the link and the colour only supports it: a link told from its sentence by colour alone is the most common failure of criterion 3.1 on the web, and the one the product audits other sites for.
## When to use, when not
Use it for a link inside a sentence or a list. For a link that acts as a call to action, use Button with `href`; for the site's navigation, the header's own links.
## Anatomy
1. An `a`, bold, in the action colour, underlined four pixels under the baseline.
## States
Plain, and inside a sentence.
## Accessibility contract
- Role and name: a link named by its text.
- Keyboard: Tab and Enter, native.
- Focus: the ring of the base layer.
- Hover: the line thickens; the colour shift is secondary.
## Content rules
The text says the destination ("la méthode", "le mode d'emploi"), never "ici" or "cliquez". A link that opens a document says its format.
## Do and don't
- Do let the underline show; never remove it to make the sentence "cleaner".
- Do not put two links with the same text and different destinations on one page.
## Examples
```svelte
Le relevé est public, écarts compris.
```
## Surfaces
- Site, app: every link in prose, as the pages move from the raw class.
- Extension: the panel's links to the criterion sheets.
## Related
Button.
## Decisions
- 2026-09-24: created from the base layer's `link-underline`, which stays for the fifty files that still write it by hand and leaves with the last one.
- 2026-09-27: `link-underline` leaves the base layer; the site and the app write this component everywhere.
# LiveRegion
---
name: LiveRegion
slug: live-region
level: atom
status: stable
surfaces: []
since: 2026-09
replaces:
- "apps/web: the aria-live paragraphs written by hand on the audit page, the settings and the admin lists, once the app is ported"
- "apps/extension: the aria-live attribute on the whole panel, which announced every redraw, when the panel is on Svelte"
criteria:
- code: "7.5"
why: "The region is in the document from the first render, empty, and only its text changes: that is the one shape every screen reader restitutes, and the component makes the other shapes impossible."
- code: "7.1"
why: "The role is chosen from the tone, status or alert, and aria-live is written with it, so the announcement is compatible with the readers that honour one or the other."
---
## Purpose
A region that announces a message that appeared after an action. What decides whether a screen reader says a status message is the document a second earlier: a region created with its text is not restituted by every reader. This component is the region, present and empty from the first render, whose text the page changes.
## When to use, when not
Use it for the outcome of an action that does not move the focus: saved, failed, three results found, copied. Do not use it for a message that takes the focus (an error summary is not a status message), for a message present at load (nothing to announce), or as a second announcement of something a control already says, such as a checked radio.
## Anatomy
1. A `div` with `role="status"` or `role="alert"` and the matching `aria-live`.
2. The message, or nothing.
## States
Polite, assertive, and hidden.
## Accessibility contract
- Role and name: `status` with `aria-live="polite"` for the ordinary case, `alert` with `assertive` for an error that blocks. No name: a live region announces its content.
- Keyboard, focus: none. A live region never moves the focus, that is what makes it one.
- Live regions: this is one. It exists from the first render; the page never mounts it at the moment of the announcement. A text identical to the previous one is not a change and announces nothing: a repeated outcome must vary, by a count or a time.
## Content rules
French. A message is one sentence that says what happened: "Audit enregistré.", "Trois pages correspondent.", "Échec de l'enregistrement : la connexion a été perdue." An error says what to do next when there is something to do.
## Do and don't
- Do mount the region with the page and change `message`; never `{#if message}` around it.
- Do use `assertive` for an error that blocks the user, and for nothing else; an assertive success harasses.
- Do not announce what a control already says: a checked radio, a pressed toggle.
## Examples
```svelte
```
## Surfaces
- App: the dashboard's filter count, the audited page's saved comment, the report's export; the form outcomes are Alerts that say they are live.
- Extension: one region per screen, mounted at boot, for the announcements the panel makes today by redrawing an `aria-live` main.
## Related
Alert, Field.
## Decisions
- 2026-09-24: a region, not a function. An `announce()` helper that mounts a region on demand is the anti-pattern this component exists to prevent; the page owns a region and writes to it.
- 2026-09-24: hidden by default. The page usually shows the outcome its own way; a visible region is for the cases where the region is the text, as in the stories.
# Logo
---
name: Logo
slug: logo
level: atom
status: stable
surfaces: [site, app, extension]
since: 2026-08
criteria:
- code: "1.1"
why: "The drawing is aria-hidden and the word carries the name: an alt on the image would make a screen reader say the brand twice."
- code: "3.2"
why: "Drawn inline in currentColor, the mark follows the ink of the theme and keeps its contrast on both sets; as an image it would stay black on the dark set."
patterns: []
related: []
---
## Purpose
The mark of RGAA Lab, "la grille", drawn inline so it follows the ink of the theme. A SVG loaded as an image is an isolated document that never inherits `currentColor`: the previous logo stayed black on the dark footer, was whitened with a filter, and vanished the day the footer went light.
## When to use, when not
Use it once in a header or a footer, and as the mark alone only where the word cannot fit (the collapsed rail of the app). Do not use it as decoration inside content.
## Anatomy
1. The mark: a square divided in four, one quarter filled, in `currentColor`, `aria-hidden`.
2. The word "RGAA Lab", which is the accessible name; visually hidden when `markOnly` is set.
## States
Three sizes and the mark-only form.
## Accessibility contract
- Role and name: an inline group whose text is "RGAA Lab". The drawing is hidden from assistive technology.
- Keyboard, focus: none of its own; wrap it in a link when it navigates.
- Contrast: inherits the ink, which is measured on every background.
## Content rules
The word is always "RGAA Lab", with "Lab" in the soft ink. Never translated, never abbreviated.
## Do and don't
- Do keep the word visible wherever there is room: it is the name.
- Do not load the mark as an image.
## Examples
```svelte
```
## Surfaces
- Site: header and footer.
- App: the lab sidebar, the auth pages.
- Extension: the store icons are generated from the same geometry by `scripts/icones.mjs`.
## Related
None.
## Decisions
- 2026-08: inline SVG rather than an image, for the theme.
- 2026-09-24: props `height` and `markOnly`; the word's size derives from the height through one custom property, the only dynamic style the component writes.
# ProgressBar
---
name: ProgressBar
slug: progress-bar
level: atom
status: stable
surfaces: [extension]
since: 2026-09
replaces:
- "apps/site: the measured case of the chargement-et-progression demo, kept as teaching material"
- "apps/extension: the hand-drawn progress strip of shell/Sidebar.svelte, and the page analysis's when the panel draws it"
criteria:
- code: "7.1"
why: "The native progress element carries its role and its value; nothing is declared by hand, and an absent value is the indeterminate state."
- code: "11.1"
why: "The label's for and the element's id come from one prop, so the bar is named like a field."
- code: "3.1"
why: "The value is written as a number next to the bar; the fill only draws it."
related: [busy-region, live-region]
---
## Purpose
A progression that can be measured, on the native element: the browser says "barre de progression, 40 %" without an ARIA attribute, and a label names it. Without a value the bar is indeterminate, the honest state for a wait whose length is unknown.
## When to use, when not
Use it for a task with a measurable advance: an export, an analysis over pages, criteria decided out of 106. For a wait with no measure inside a zone, BusyRegion; for a button waiting on the server, Button with `busy`.
## Anatomy
1. The label.
2. The bar, hollow, filled with the action ink.
3. The percentage, written.
## States
Measured and running, indeterminate, done, and the strip across a shell's edge.
## Accessibility contract
- Role and name: `progressbar`, named by the label, the value the element's own.
- Keyboard, focus: none.
- Live regions: none here. The bar carries the value continuously; announcing every percent would be chatter. Milestones are the page's through LiveRegion: started, half, done, and done matters most, since without it the user who does not see the screen waits for a result already there.
## Content rules
French. The label is what progresses, as a noun phrase: "Export du rapport".
## Do and don't
- Do announce the end through a live region.
- Do not draw the bar with a div and `aria-valuenow`; the element exists.
- Do not hide the percentage unless the label already says it.
- Do use `strip` only where the screen says the progress in words next to it, as the panel's sentence "38 % des critères évalués" does; the strip alone would leave the figure to colour.
## Examples
```svelte
```
## Surfaces
- Extension: the strip of the audit's progress over the panel's header; the page analysis, when the panel is on Svelte.
- App: the bulk export; not ported yet.
## Related
BusyRegion, LiveRegion.
## Decisions
- 2026-09-24: the native element, styled through its pseudo-elements, since a remade bar would need three ARIA attributes for what the browser already says.
- 2026-09-27: `strip` draws the same element as the thin line across the extension panel's top, which was a `div` with `role="progressbar"` and three ARIA attributes; the label is hidden rather than dropped, so the bar keeps its name.
# Prose
---
name: Prose
slug: prose
level: atom
status: stable
surfaces: [site, app]
since: 2026-08
criteria:
- code: "10.11"
why: "The measure is a maximum in characters, not a fixed width: the paragraph reflows at 320 pixels without horizontal scrolling."
patterns: []
related: [heading, eyebrow, inline-markup]
---
## Purpose
A paragraph of running text, in the soft ink, with its line length bounded. Beyond about 58 characters the eye loses the next line coming back to the margin; the component sets that bound so that no page has to remember it.
## When to use, when not
Use it for the sentences under a heading, an introduction, a caption of more than one line. Do not use it for interface labels, which belong to their control, nor for lists or tables.
## Anatomy
1. A paragraph, soft ink, with a maximum width in characters.
## States
Three sizes and a measure; no interaction.
## Accessibility contract
- Role and name: a paragraph.
- Keyboard, focus, live regions: none.
- The soft ink measures 7.32:1 on the page and 7.64:1 on a surface (tokens/contrast.json).
## Content rules
French, full sentences, one idea per sentence. No "il est important de".
## Do and don't
- Do let it wrap; a measure is a maximum, never a fixed width.
- Do not put a heading's job in it: a bold first sentence is not a heading.
## Examples
```svelte
RGAA Lab est une extension Chrome et Firefox et une application web qui guident
l'audit des 106 critères du RGAA 4.1.2.
```
## Surfaces
- Site: 38 files.
- App: the error page.
## Related
Heading, Eyebrow, InlineMarkup.
## Decisions
- 2026-08: 58 characters by default, the site's article measure being 66 and the page rule 70; a paragraph under a heading reads best shorter than a column of prose.
- 2026-09-24: `rl-paragraph` rules on the tokens; the class is not `rl-prose`, which the base layer reserves for article typography. The measure stays the one dynamic style.
# ScrollRegion
---
name: ScrollRegion
slug: scroll-region
level: atom
status: stable
surfaces: [site, app]
since: 2026-08
replaces:
- "apps/web: nine hand-written scroll wrappers on the dashboard, the site page and the report"
criteria:
- code: "12.9"
why: "A region that scrolls takes the focus, so its off-screen columns are reachable with the keyboard alone."
- code: "12.8"
why: "The region has a name, so the tab stop is not a silent one."
patterns: [data-table-page]
related: [data-table]
---
## Purpose
A zone that scrolls and can be reached with the keyboard. A `div` in `overflow: auto` never takes the focus: a table wider than its column scrolls with the mouse, the wheel, a finger, and not with the keyboard, so its off-screen columns are out of reach. The defect was found on the report, five zones, and again on the site and the app.
## When to use, when not
Wrap anything that may overflow sideways: a wide table, a code block that must not wrap, a matrix. Do not wrap content that never overflows; a focusable region with nothing to scroll is a stop for nothing.
## Anatomy
1. A `div` with `tabindex="0"`, `role="region"` and a name.
2. Optionally a hairline frame.
## States
Named by a label, named by an existing heading, framed or not.
## Accessibility contract
- Role and name: a region named "X (zone défilante)", or named by the heading given in `labelledBy`. One of the two is required; without a name the stop is silent.
- Keyboard: Tab reaches it, the arrow keys scroll it.
- Focus: the ring of the base layer.
- Live regions: none.
## Content rules
The label says what one enters ("Tableau des critères"), not the fact that it scrolls: the component adds that word.
## Do and don't
- Do give the label the table's caption or the section's heading.
- Do not put interactive content only reachable by scrolling without a name on the region.
## Examples
```svelte
...
```
## Surfaces
- App: the dashboard, the project page, the admin lists, the comparison, the report.
- Site: the coverage tables, the pricing grid.
## Related
DataTable.
## Decisions
- 2026-08: the Svelte warning on a non-interactive tabindex is silenced here, once, rather than in nine templates.
- 2026-09-24: renamed from ZoneDefilante, props `label`, `labelledBy`, `framed`; the frame is a modifier class on the tokens.
# Select
---
name: Select
slug: select
level: atom
status: stable
surfaces: []
since: 2026-09
replaces:
- "apps/site: the liste-déroulante demo, kept as teaching material"
- "apps/web: the hand-built selects of the audit's report parameters, the new audit and the new account"
criteria:
- code: "11.1"
why: "The label's for and the control's id come from one prop, so a select cannot be rendered without its label."
- code: "11.10"
why: "The error is tied by aria-describedby and aria-invalid together, and an empty option keeps nothing chosen distinct from a choice."
- code: "7.3"
why: "The native element gives the arrows, the first letters and the system's picker; nothing is remade, so nothing is lost."
patterns: [form-with-error-summary]
related: [field, radio-group]
---
## Purpose
A choice among options, as the native `select` and nothing else. The browser gives the focus, the arrows, typing the first letters, the system's picker on a phone; a remade select loses all of it. The component ties the label, the hint and the error, and keeps an empty option so nothing chosen stays distinct from a choice.
## When to use, when not
Use it for a choice among many options, or among a few that need no comparing. For two to five options the user should see at once, RadioGroup. For free text, Field.
## Anatomy
1. The label, with the required mark when needed.
2. The hint.
3. The select, with the empty option first, the options or the groups.
4. The error.
## States
Plain, with a choice made, grouped, required, in error, and unavailable.
## Accessibility contract
- Role and name: a listbox in a combobox, as the browser exposes a select, named by its label; "(obligatoire)" in the name when required.
- Keyboard, focus: the browser's. No navigation on change: the arrows walk the options one by one, and a navigation fired there would leave on the first one met.
- Live regions: the error is `role="alert"`.
## Content rules
French. The label is a noun ("Niveau visé"); the empty option says what to do ("Choisissez", "Choisissez un critère"); option texts are short and start with what distinguishes them.
## Do and don't
- Do keep the empty option unless a value is always chosen, as with a single supported version.
- Do not navigate or submit on change.
- Do not remake the control with a list of divs.
## Examples
```svelte
```
## Surfaces
- App: the audit's method and version, the new account's role.
- Extension: the page switcher's list, when the panel is on Svelte.
## Related
Field, RadioGroup.
## Decisions
- 2026-09-24: `placeholder` defaults to "Choisissez" and an empty string removes it, so the ordinary case asks and the exception is written.
# SkipLink
---
name: SkipLink
slug: skip-link
level: atom
status: stable
surfaces: [design, site, app]
since: 2026-09
replaces:
- "packages/design base.css: the skip-link class, and its three raw uses on the design site, the public site and the lab layout"
criteria:
- code: "12.7"
why: "It is the skip link to the main content the criterion asks for, first in the tab order, and it cannot be rendered without a target."
- code: "12.6"
why: "The header and the navigation, present on every page, can be avoided in one keystroke."
- code: "10.7"
why: "The link appears on focus, in the action colour on its own fill: the stop is visible, which a link hidden with display none never is."
patterns: [app-shell]
related: [link]
---
## Purpose
The first stop of the page, hidden until focused: one link that lets a keyboard user jump over the header and the navigation to the main landmark. Hidden off-screen rather than removed, since `display: none` would take it out of the tab order, which is precisely what it exists to repair.
## When to use, when not
Once per page, as the first element of the body, before the header. Not for jumping inside the content; that is a table of contents.
## Anatomy
1. An `a` with the target's id as its `href`, in the action colour on its own fill, positioned above the page until focused.
## States
Hidden, and focused; with the default text, and with another.
## Accessibility contract
- Role and name: a link, named by its text.
- Keyboard, focus: the first Tab of the page reaches it; Enter moves the focus to the target. The target must carry `tabindex="-1"`: without it the page scrolls and nothing takes the focus, and the next Tab starts from wherever the browser decides. The site's sheet on the subject measures the difference.
- Live regions: none.
- Print: the link leaves the paper; a printed page has no focus to move.
## Content rules
French. "Aller au contenu" for the main landmark; for another target, "Aller à" followed by what it is ("Aller aux résultats").
## Do and don't
- Do give the target `tabindex="-1"`, and only that: a `-1` is never a Tab stop.
- Do put the link first in the body, before the header, so it is the first stop.
- Do not hide it with `display: none` or `visibility: hidden` when idle.
## Examples
```svelte
……
```
## Surfaces
- Design site, public site and the lab layout: the one link at the top of each shell.
## Related
Link.
## Decisions
- 2026-09-24: written as a component and the base-layer rule retired with its last raw use, so the three shells stop copying a class and a text.
# StatusChip
---
name: StatusChip
slug: status-chip
level: atom
status: stable
surfaces: [site, app]
since: 2026-08
replaces:
- "apps/web: status badges written by hand in the report and the audit pages"
criteria:
- code: "3.1"
why: "The abbreviation and the word are part of the component; a chip that says its status by colour alone cannot be built with it."
- code: "3.2"
why: "Each status ink is measured on the page, the surface and the hollow (tokens/contrast.json)."
patterns: []
related: [conformity-rate, icon]
---
## Purpose
The status of a criterion: conforme, non conforme, non applicable, non testé. It exists to guarantee that the word is always there. Criterion 3.1 forbids information by colour alone, and that is the rule one forgets first when writing a badge by hand in a corner of a page.
## When to use, when not
Use it wherever a criterion's status is shown: a grid, a report, a column header. Do not use it for anything that is not one of the four audit statuses; a derogation, a "to recheck" mark or a level badge are other components.
## Anatomy
1. The status drawing from the icon set, `aria-hidden`.
2. The abbreviation, in the mono face.
3. The word, visible or visually hidden when `short` is set.
4. A line around, solid; dashed for "non testé", which is not a verdict.
## States
The four statuses, and the short form.
## Accessibility contract
- Role and name: an inline span; the name is "C Conforme" and so on, the drawing excluded. With `short`, the word stays in the name.
- Keyboard, focus, live regions: none.
- The chip never wraps: a status reads in one piece or not at all.
- Labels come from `@rgaa-dev/core`, the same table as the extension and the reports; drawings from `statusIcon`.
## Content rules
The four French words of the standard, never a synonym. The abbreviation is the standard's too.
## Do and don't
- Do use `short` in a column header, where the word is in the header's context anyway.
- Do not give "non testé" a colour of its own: it is an absence of verdict.
## Examples
```svelte
```
## Surfaces
- App: the dashboard headers, the audit page, the page view, the comparison, the report.
- Site: the method page and the gallery.
## Related
ConformityRate, Icon.
## Decisions
- 2026-08: the symbol for non conforme is ✕ (U+2715), from core; the extension had drifted to ✗.
- 2026-09-24: props `status` and `short`; the symbol character gives way to the icon drawing, the same shape a control shows for that status, while documents and text keep the character from core.
# Switch
---
name: Switch
slug: switch
level: atom
status: stable
surfaces: []
since: 2026-09
replaces:
- "apps/site: the interrupteur demo, kept as teaching material"
- "apps/extension: the hand-made toggles of the panel's settings, when it is on Svelte"
criteria:
- code: "7.1"
why: "A button with the switch role and aria-checked written from the state on every render: the attribute cannot go stale, and the product's engine checks it is there."
- code: "7.3"
why: "A button toggles on Enter and Space and Tab reaches it; nothing is written by hand for the keyboard."
- code: "3.1"
why: "The state is told by the thumb's position as much as by the track's colour, and under forced colours by the system's palette."
related: [checkbox]
---
## Purpose
A setting that applies at once, as a button with `role="switch"` and `aria-checked` written from the state. Its name is its label, inside it. No submit button: the caller acts on `onchange`.
## When to use, when not
Use it for a preference that takes effect the moment it is flipped: show the criterion numbers, sync automatically. Not for a choice submitted with a form (Checkbox), nor for anything that needs a confirmation.
## Anatomy
1. The button, holding the label and the drawing.
2. The label, first, as the setting's name.
3. The track and the thumb, hidden from assistive technology.
4. The hint, under, tied by `aria-describedby`.
## States
Off, on, with a hint, and unavailable.
## Accessibility contract
- Role and name: `switch`, named by its label text; `aria-checked` says on or off.
- Keyboard, focus: Tab reaches it, Enter and Space flip it; the ring is the system's.
- Live regions: none; the switch announces its own state. The page says the consequence its own way when it matters.
- Forced colours: the track and the thumb take ButtonText, Highlight and HighlightText.
## Content rules
French. The label is the setting as a noun phrase ("Numéros de critère"), never a verb that would change with the state.
## Do and don't
- Do act in `onchange`; a switch that waits for a button is a checkbox.
- Do not write "Activer" on a switch: the state is already said.
- Do not announce the flip in a live region; the control already does.
## Examples
```svelte
```
## Surfaces
- Extension: the panel's display settings, when it is on Svelte.
- App: the notification preferences; not ported yet.
## Related
Checkbox.
## Decisions
- 2026-09-24: first written as a native checkbox with the switch role, the other implementation the site's demo shows. The product's own engine refused it on the design site: ACT rule 4e8ab6 requires `aria-checked` on any element with the switch role, and ARIA in HTML forbids writing it on a native checkbox. The button carries the attribute without conflict, and the state cannot go stale since it is written at every render.
# Text
---
name: Text
slug: text
level: atom
status: stable
surfaces: [app, extension, site]
since: 2026-09
replaces:
- "apps/web: the text-sm text-ink-soft utility pairs on every screen's help lines and captions"
- "apps/extension: the hand-written caption and hint classes of the panel's screens"
criteria:
- code: "3.1"
why: "A tone is an ink; the words carry the state, and the doc refuses a tone on a sentence that does not say it."
- code: "10.4"
why: "Sizes are from the scale in rem, so the text zooms with the browser's setting."
patterns: []
related: [prose, heading, eyebrow]
---
## Purpose
A run of text with a size and a tone: a caption, a count, a help line, a sentence that says a state. One element, a size from the scale, an ink from the tokens.
## When to use, when not
Use it for the short text a screen is full of. Not for a paragraph of running text, which is Prose and takes a measure; not for a heading; not for a label above small capitals, which is Eyebrow.
## Anatomy
1. One element, `p`, `span` or `div`, with the size, the tone, the weight and the face as modifiers.
## States
Plain, soft and small, the three state tones, and inline with a bold word and a mono value.
## Accessibility contract
- Role and name: none; text.
- Keyboard, focus: none.
- Live regions: none. A sentence that must be announced goes through LiveRegion, which can hold a Text.
- A tone never carries the meaning: "Erreur, la capture a échoué." says it before the ink does.
## Content rules
French. A sentence ends with a period; a caption or a count does not. A state tone starts with the word of the state when the sentence is short ("Attention, ...").
## Do and don't
- Do use `soft` for what supports the main text, never for the main text.
- Do use `mono` for a value the reader may copy: a selector, an address.
- Do not use a state tone to decorate; the ink says a state, and only when the words say it too.
## Examples
```svelte
Visible dans le rapport.
Le sélecteur footer .legal-links désigne trois éléments.
```
## Surfaces
- App: help lines, captions, counts.
- Extension: the panel's hints and captions.
- Site: captions under the demos.
## Related
Prose, Heading, Eyebrow.
## Decisions
- 2026-09-26: no measure prop: a run of text that needs one is a paragraph of running text, which is Prose.
# Tooltip
---
name: Tooltip
slug: tooltip
level: atom
status: stable
surfaces: []
since: 2026-09
replaces:
- "apps/site: the infobulle demo, kept as teaching material"
criteria:
- code: "10.13"
why: "The bubble can be dismissed with Escape without moving the pointer or the focus, the pointer can enter it, and it stays while the trigger has the focus or the hover."
- code: "7.1"
why: "The bubble is the trigger's accessible description through aria-describedby, read at focus even while hidden; the trigger keeps its own name."
- code: "7.3"
why: "The trigger is a button: the keyboard reaches it and the description is read at focus, with no hover needed."
related: [kbd, inline-markup]
---
## Purpose
A description for a term that already has its name. The trigger keeps its text as its name and the bubble is its description, tied by `aria-describedby`, read at focus whether shown or not; the `title` attribute gives no such guarantee.
## When to use, when not
Use it for a definition or a precision on a term in running text: a rate, a status, an abbreviation. Not for the name of an icon-only control (that is IconButton's `label`), not for anything the reader must not miss (write it in the text), never with something interactive inside.
## Anatomy
1. The group, which holds the trigger and the bubble so the pointer never leaves.
2. The trigger, a button with the term as its text, underlined with dots.
3. The bubble, `role="tooltip"`, hidden at rest.
## States
In a sentence, and a short one.
## Accessibility contract
- Role and name: the trigger is a button named by its text; the bubble is its description. `role="tooltip"` documents the intent.
- Keyboard, focus: the bubble shows on focus and hides when the focus leaves; Escape hides it and moves nothing. A click toggles it.
- Pointer: the bubble shows on hover of the trigger and stays while the pointer is on the trigger or in the bubble.
- Live regions: none; a bubble that spoke at every mouse move would make the page unlistenable.
## Content rules
French. One or two sentences that define or precise; no link, no button inside.
## Do and don't
- Do put the essential in the text and the precision in the bubble.
- Do not use it to name a control.
- Do not open it on a delay or close it on a timer.
## Examples
```svelte
Son taux de conformité se lit en pourcentage.
```
## Surfaces
- App: the report's terms; not ported yet.
- Site: the sheets' definitions, once the site is ported.
## Related
Kbd, InlineMarkup.
## Decisions
- 2026-09-24: the refusal by Escape is forgotten once the pointer and the focus have both left, as the site's demo does; without it a bubble closed once never came back.
# Accordion
---
name: Accordion
slug: accordion
level: molecule
status: stable
surfaces: []
since: 2026-09
replaces:
- "apps/site: the accordéon demo, kept as teaching material"
criteria:
- code: "7.1"
why: "Each panel is a Disclosure: a native button with aria-expanded and aria-controls, the state and the relation assistive technology already knows."
- code: "9.1"
why: "Every title is a heading at the level the page gives, so the set reads as a list of sections in the outline."
- code: "12.8"
why: "A folded panel carries hidden and its content leaves the tab order, whatever the number of panels."
patterns: []
related: [disclosure, tabs]
---
## Purpose
A list of disclosures under their headings, each independent by default: opening one closes no other, because closing what the reader had just opened surprises them. The items are data and the panel is a snippet, so the caller writes the content once and the component holds which are open.
## When to use, when not
Use it for a list of questions and answers, a list of criteria with their explanation, any set of details the reader may skip. Do not use it for views of equal weight the user switches between (Tabs), nor for one panel alone (Disclosure).
## Anatomy
1. One Disclosure per item: a heading with the button, and the panel.
## States
Independent, exclusive, and with a panel open at first.
## Accessibility contract
- Role and name: Disclosure's, per panel.
- Keyboard, focus: Tab moves from button to button and into an open panel's content; Enter and Space toggle. No arrow-key navigation: the panels are headings and buttons, and both already have their keys.
- Live regions: none.
## Content rules
French. Titles are questions or noun phrases, each different from the others so the list of headings reads on its own.
## Do and don't
- Do give `level` the level the titles hold in the page.
- Do not set `exclusive` unless the panels really are alternatives; it costs a screen reader user the panel they opened to compare.
## Examples
```svelte
{#snippet panel(item)}
{answers[item.id]}
{/snippet}
```
## Surfaces
- App: the questions of the help page and the criteria of the report; not ported yet.
- Site: the questions of the pricing page, once the site is ported.
## Related
Disclosure, Tabs.
## Decisions
- 2026-09-24: independent by default, exclusive on request, as the site's accordéon demo argues.
# Alert
---
name: Alert
slug: alert
level: molecule
status: stable
surfaces: [site, app]
since: 2026-08
replaces:
- "apps/web: three hand-built inline notices on the audit page, the report and the page view"
criteria:
- code: "3.1"
why: "Every tone carries a readable word (Erreur, Attention, Fait, Information) and a drawing; a red box with no word says nothing to whoever does not see the hue."
- code: "7.1"
why: "The ARIA role is chosen from the tone: alert interrupts, status waits for a pause, and a message present at load announces nothing."
patterns: [form-with-error-summary]
related: [callout, icon]
---
## Purpose
A message of information, success, warning or failure. It exists for three reasons, two of which are accessibility traps a hand-written box almost always misses: the word before the colour, the right ARIA role, and a decided `live`.
## When to use, when not
Use it for a message the page shows about an action or a state: a form error at the top of the form, a confirmation after a save, a warning before a destructive step. Do not use it for a transient toast, for content that is merely framed (that is Callout), or for a field's own error (Field carries it).
## Anatomy
1. The tone's drawing from the icon set, `aria-hidden`.
2. The tone's word, visible, or visually hidden before a free title.
3. The message.
4. A line around, in the tone's ink, never a fill.
## States
Four tones, with or without a title, live or not.
## Accessibility contract
- Role and name: no role when the message is present at load (it is read in the flow). With `live`, `alert` for an error and `status` for everything else. The tone's word is always in the accessible name.
- Keyboard, focus: none. A live message never moves the focus.
- Live regions: a message that appears after an action is announced; a message present at load is not. That is what `live` decides, and it must be decided.
## Content rules
French. The message says what happened and, for an error, what to do. The tone's word is the standard's (Erreur, Attention, Fait, Information); a title, when there is one, is specific.
## Do and don't
- Do set `live` on a message that appears in response to an action.
- Do not set `live` on a message rendered with the page: it would be announced twice.
- Do not use the warning tone for a decoration; the tone is the meaning.
## Examples
```svelte
Le mot de passe ne correspond pas.Les 106 critères sont à jour.
```
## Surfaces
- App: the auth pages, the settings, the admin lists, the audit page.
- Site: the beta page and the gallery.
## Related
Callout, Icon.
## Decisions
- 2026-08: `alert` only for the error tone; a success announced as an alert harasses.
- 2026-09-24: props `tone` (`info`, `success`, `warning`, `error`) and `title`; the drawing comes from the icon set, and the warning tone takes the attention ink the tokens define for it, where it used to borrow the plain ink.
# AnswerButtons
---
name: AnswerButtons
slug: answer-buttons
level: molecule
status: stable
surfaces: [extension]
since: 2026-09
replaces:
- "apps/extension: the five families of answer buttons (verdict, answer, final answer, inline answer, question 31) the wizards wrote"
criteria:
- code: "7.1"
why: "A group of toggle buttons: role group named by the question, aria-pressed on each, a real button element that the keyboard operates."
- code: "3.1"
why: "The retained answer is said by aria-pressed and drawn by a fill, a change of shape; it never rests on a hue alone."
- code: "12.10"
why: "The single-key shortcut of each answer is printed on the button, next to what it drives."
related: [question, kbd, wizard-footer]
---
## Purpose
The buttons a wizard's question is answered with: Oui, Non, and sometimes Non applicable, one group named by the question, the retained one said by its state and drawn by its fill.
## When to use, when not
Use it for every answer a wizard takes with a click. Not for a verdict the tool concluded itself (AutoVerdict), not for a form's radio group, which is RadioGroup.
## Anatomy
1. A group, named by the question.
2. One button per answer, with its label and its shortcut key.
## States
Nothing retained, Oui retained, and three answers with Non applicable.
## Accessibility contract
- Role and name: `role="group"` named by the question through `aria-labelledby`; each answer is a `button` with `aria-pressed`.
- Keyboard, focus: Tab reaches each button, Enter or Space presses it; the letter shortcuts are the page's listener, not the buttons'.
- Live regions: none; the wizard says what the answer did, if anything.
## Content rules
French. Oui, Non, Non applicable; never Valider or OK. The key is the capital letter printed on the key cap.
## Do and don't
- Do name the group by the question's id.
- Do not add a check character to the retained label: the state is the attribute, and a reader spells characters out.
## Examples
```svelte
(answer = v)} answers={[{ value: 'C', label: 'Oui', key: 'O' }, { value: 'NC', label: 'Non', key: 'N' }]} />
```
## Surfaces
- Extension: every wizard question.
## Related
Question, Kbd, WizardFooter.
## Decisions
- 2026-09-26: one component for the five families of answer buttons the wizards had grown, so the keyboard shortcuts and the retained state have one shape.
# AutoVerdict
---
name: AutoVerdict
slug: auto-verdict
level: molecule
status: stable
surfaces: [extension]
since: 2026-09
replaces:
- "apps/extension: insererOrigineDuVerdict and the auto-verdict blocks each wizard wrote"
criteria:
- code: "3.1"
why: "The status is the word in bold; the line and the drawing's ink confirm it and never carry it alone."
- code: "7.1"
why: "Showing the element is a real button, with the selector as data, not a link to nowhere."
related: [question, status-chip, answer-buttons]
---
## Purpose
A verdict the tool concluded itself: the status, who said it, what it saw, and the reminder that the auditor's answer wins.
## When to use, when not
Use it above the answer buttons when the engine or the page's markup already settled a criterion, or in place of the buttons when the criterion is the sum of answers the auditor gave elsewhere, element by element on a walk. Not for the auditor's answer to the question on screen, which the buttons show.
## Anatomy
1. The verdict line: the drawing, the status in bold, the source.
2. The findings, one per line, with a "Montrer" button when the element can be shown.
3. The closing sentence, or what the wizard says instead.
## States
Conforming from the page, non-conforming from the engine with findings, not applicable with a sentence of the wizard's, and non-conforming from the auditor's own answers with the sentence that says where to change them.
## Accessibility contract
- Role and name: a block of text; "Montrer" buttons named by their word, with the selector as data.
- Keyboard, focus: the buttons only.
- Live regions: none; the block is part of the screen.
## Content rules
French. The status word from `@rgaa-dev/core`; the source is "d'après le moteur de détection", "d'après la page" or "d'après vos réponses"; a finding is a reason in words, the selector never printed. With the auditor as source, the closing sentence must say where the answers are changed, since the reminder that the answer wins would point at buttons that are not there.
## Do and don't
- Do keep the selector in the button: a ten-level path printed drowned the reason.
- Do not present the verdict as final; the closing sentence says the auditor's answer wins.
## Examples
```svelte
```
## Surfaces
- Extension: the wizards, when a criterion was settled before the auditor.
## Related
Question, StatusChip, AnswerButtons.
## Decisions
- 2026-09-26: the findings are the engine's own text, split into reason and selector by the wizard; the component prints the reason and keeps the selector for the button.
- 2026-09-26: a third source, the auditor. The script wizard concludes 7.1 and 7.3 from the checks answered component by component, and its questions screen shows them read-only; "d'après la page" was false there, and a local block would have been a second implementation of this one.
# Banner
---
name: Banner
slug: banner
level: molecule
status: stable
surfaces: []
since: 2026-09
replaces:
- "apps/web: lib/components/BandeauBeta.svelte, gone with the lab layout's port"
criteria:
- code: "12.6"
why: "A named aside: the banner is a landmark that can be reached or skipped, and it does not impose itself on the reading order."
- code: "7.5"
why: "No alert role and no live region: nothing in a banner is urgent, and announcing it would interrupt the reading for what does not deserve it."
- code: "3.1"
why: "The word carries the status and the line on the left only confirms it."
- code: "11.9"
why: "The close button is a real button named 'Masquer l'annonce', reachable at the keyboard."
patterns: [app-shell]
related: [alert, icon-button]
---
## Purpose
An announcement at the top of a shell, dismissable and remembered: the beta, a maintenance, a change of terms. A named aside with a word, a sentence, a link when there is more, and a close button whose choice is kept under a key.
## When to use, when not
Use it for an information that concerns every page and can wait: a date, a maintenance. Not for the outcome of an action (LiveRegion or Toast), not for an error (Alert), not for anything the user must answer (Dialog).
## Anatomy
1. The aside, with the line on the left.
2. The word, which names it.
3. The text, with its link.
4. The close button.
## States
The beta, and a maintenance.
## Accessibility contract
- Role and name: `complementary`, named by the word through `aria-labelledby`. No `role="alert"`, no `aria-live`.
- Keyboard, focus: the close button is a Tab stop; closing moves the focus nowhere in particular, the banner was not focused.
- Persistence: the dismissal is stored under `dismissKey`; a new key brings the banner back once.
## Content rules
French. The word is one noun ("Bêta"). The text says the fact and the date; no countdown, no red, no "plus que N jours".
## Do and don't
- Do put the date in the key when the banner carries one, so an extension of the date shows the banner again.
- Do not show more than one banner at a time.
- Do not animate it in.
## Examples
```svelte
L'application est gratuite jusqu'au {date}. {promise} Voir les tarifs
```
## Surfaces
- App: the beta banner of the lab, in the AppShell's banner slot.
## Related
Alert, IconButton.
## Decisions
- 2026-09-24: closed at the first render and opened once storage is read, as the app's banner does, so whoever closed it never sees it flash.
# Breadcrumb
---
name: Breadcrumb
slug: breadcrumb
level: molecule
status: stable
surfaces: []
since: 2026-09
replaces:
- "apps/web: lib/components/Breadcrumb.svelte, gone"
- "apps/site: the fil-d-ariane demo, kept as teaching material"
criteria:
- code: "12.6"
why: "A navigation landmark with its own name, so the path can be reached or skipped like the other regions."
- code: "9.3"
why: "The steps are an ordered list with its role restored, so the count and the rank are announced."
- code: "3.1"
why: "The current page is told by aria-current, not only by its weight; the links by their underline, not only by their colour."
patterns: [app-shell]
related: [link]
---
## Purpose
The path to the current page, as a named navigation landmark holding a list, the separators hidden and the last step marked current. It looks like a row of links with slashes; what tells them apart is what assistive technology receives.
## When to use, when not
Use it on any page more than one level deep in the app. Not on the first level, where it would hold one step, and not as a substitute for a page title.
## Anatomy
1. A `nav` named "Fil d'Ariane".
2. An ordered list, one item per step.
3. Links for every step but the last; the last step with `aria-current="page"`.
4. Hidden separators.
## States
A short path, and a deep one that wraps.
## Accessibility contract
- Role and name: `navigation`, named by `label`; a list inside, with `role="list"` restored.
- Keyboard, focus: the links are ordinary links.
- Live regions: none.
- Print: the trail leaves the paper; it is a way through the screen.
## Content rules
French. Each step is the title of the page it leads to, as short as the page allows; the last step is the current page's title.
## Do and don't
- Do give the last item no `href`: it is where the user is.
- Do not repeat the breadcrumb in the footer.
## Examples
```svelte
```
## Surfaces
- App: every page under an organisation, a site or an audit, the settings, and the administration, which had none.
## Related
Link.
## Decisions
- 2026-09-24: the separator is a hidden slash, drawn the same as the hand-made version the site's demo compares it with, so the difference is only what is restituted.
# Callout
---
name: Callout
slug: callout
level: molecule
status: stable
surfaces: [site]
since: 2026-08
criteria:
- code: "3.1"
why: "The status changes only the line; a status colour on the frame never carries the information alone, the words inside do."
- code: "3.2"
why: "No fill, so there is no second background whose contrast would have to be checked."
patterns: []
related: [alert]
---
## Purpose
A block framed by a structural line. It was rewritten by hand about fifteen times during the redesign, each page copying the same border style, and a copied shape diverges. The status changes the colour of the line, never a fill.
## When to use, when not
Use it to set a passage apart: a definition, an aside, a quoted rule, a block that borrows a status colour with its word inside. Do not use it for a message about an action (that is Alert) nor as a card in a grid.
## Anatomy
1. A `div`, or an `aside` when the content is beside the point.
2. A line around: strong or subtle, neutral or in a status colour.
## States
Strong or subtle line, neutral or in the NC or C colour, as an aside or not.
## Accessibility contract
- Role and name: none for a `div`; a complementary landmark for an `aside`, so use `aside` only for content that is truly aside.
- Keyboard, focus, live regions: none.
## Content rules
A block in a status colour says the status in words inside it.
## Do and don't
- Do use the subtle line for an aside and the strong one for an argument.
- Do not fill it: the direction refuses colour slabs.
## Examples
```svelte
Une définition qui mérite un cadre.Un aparté, vraiment à côté du propos.Non conforme : le lien n'a pas d'intitulé.
```
## Surfaces
- Site: the criteria sheets, the pricing page.
## Related
Alert.
## Decisions
- 2026-08: the line, never a fill.
- 2026-09-24: renamed from Encadre, props `rule`, `status`, `aside`; the line is modifier classes, so a prop that changes after mount redraws, which the style computed once did not.
# Card
---
name: Card
slug: card
level: molecule
status: stable
surfaces: [site]
since: 2026-08
criteria:
- code: "6.1"
why: "When the card is a link, the whole card is the link and its text is the name: a clickable zone the size of a word is a defect of ergonomics as much as of accessibility."
- code: "9.3"
why: "Inside a list grid the card is a list item; the grid tells it, so a ul can never hold an article by mistake."
patterns: []
related: [card-grid]
---
## Purpose
A card laid on the surface, separated from its neighbours by the hairline of the grid. With `href`, the whole card becomes the target.
## When to use, when not
Use it inside a CardGrid for a set of comparable items: articles, services, capabilities. Do not use it alone as a framed box (that is Callout) or as a form wrapper.
## Anatomy
1. The item: `li` in a list grid, `article` otherwise. The grid decides.
2. The content, padded; a link covering the whole card when `href` is set.
## States
Plain, as a link, inside a list or not.
## Accessibility contract
- Role and name: a list item or an article; with `href`, a link whose name is the card's text.
- Keyboard: the link is one tab stop for the whole card.
- Focus: the ring of the base layer around the link.
- Live regions: none.
## Content rules
A card that is a link opens with its title, so the link's name starts with what it leads to.
## Do and don't
- Do let the grid decide `listItem`; set it by hand only outside a grid.
- Do not put a second link or a button inside a card that is a link.
## Examples
```svelte
Les outils qui testentLa méthode
```
## Surfaces
- Site: 12 files (articles, services, capabilities).
- App: every form's card, the empty states, the site and page cards that link, the sessions and tokens as list items; the report keeps the base layer's class until it is a document.
## Related
CardGrid.
## Decisions
- 2026-08: the grid passes a flag during the construction of its cards rather than a Svelte context, which threw at hydration and blanked the pages (see `context.ts`).
- 2026-09-24: prop `listItem`; the surface and the padding are `rl-card` rules on the tokens.
- 2026-09-24: the card draws its own hairline as an outline, so a grid needs no background and an incomplete row shows no painted cell.
# CardGrid
---
name: CardGrid
slug: card-grid
level: molecule
status: stable
surfaces: [site]
since: 2026-08
criteria:
- code: "9.3"
why: "An enumeration of cards is a real list (ul with role list), announced with its count; the grid passes the fact to its cards so the markup cannot be half a list."
patterns: []
related: [card]
---
## Purpose
A grid of cards separated by a one-pixel hairline. Each card carries its line as an outline and the outlines meet in the one-pixel gap: the separation is a real stroke, not a border doubled between two neighbours, and an incomplete row shows no painted cell. Structure by the line, not by the shadow.
## When to use, when not
Use it for two to four comparable cards side by side. Make it a list as soon as the cards are an enumeration. Do not use it for a single card.
## Anatomy
1. A `ul` with `role="list"` (list mode) or a `div`.
2. A grid with a one-pixel gap where the cards' outlines meet; two, three or four columns from the small breakpoint, four folding to two below the large one.
## States
Two to four columns, list or not.
## Accessibility contract
- Role and name: a list when `list` is set, announced with its number of items; a plain group otherwise. `role="list"` is explicit because a reset that removes bullets makes some screen readers drop the list semantics.
- Keyboard, focus, live regions: none of its own.
## Content rules
None.
## Do and don't
- Do choose the list form for enumerations: three capabilities, five articles.
- Do not put anything but cards inside.
## Examples
```svelte
.........
```
## Surfaces
- Site: 12 files.
- App: the organisation's sites, as a list grid.
## Related
Card.
## Decisions
- 2026-08: the grid announces to its cards whether they are list items, so the two choices cannot disagree.
- 2026-09-24: props `columns` and `list`; the grid and its breakpoints are `rl-card-grid` rules.
- 2026-09-24: the hairline moved from a background behind the grid to an outline on each card, because the background painted the empty cells of a five-card row in three columns on the public site.
# CommentField
---
name: CommentField
slug: comment-field
level: molecule
status: stable
surfaces: [extension]
since: 2026-09
replaces:
- "apps/extension: the comment textarea each wizard wrote with inline styles and the shared id a-11y-wizard-comment"
criteria:
- code: "11.1"
why: "The textarea has a visible label tied by for and id, given by the page so two fields never share one."
- code: "11.2"
why: "The label says what the note is about, the element judged, and says when it is optional."
- code: "11.10"
why: "The hint under the label says the keys before the auditor types: Enter advances, Shift and Enter breaks the line."
related: [field, answer-buttons, wizard-footer]
---
## Purpose
The auditor's note on the element judged: a small textarea with its label, in every wizard.
## When to use, when not
Use it for the note that travels with an answer. Not for a form of the app, which is Field with `multiline`.
## Anatomy
1. The label, with "(facultatif)" when it is.
2. The hint line, optional.
3. The textarea, which grows with the text.
## States
Empty, filled, and with a hint about the keys.
## Accessibility contract
- Role and name: a `textarea` named by its `label`, described by the hint when there is one.
- Keyboard, focus: an ordinary text field. The wizard's shortcut listener leaves letters alone inside it and takes Enter to advance, Shift and Enter to break the line; the hint says so.
- Live regions: none.
## Content rules
French. The label names the element judged ("Commentaire sur ce cadre"); the placeholder is "Ce que vous avez constaté…".
## Do and don't
- Do give each field an id of its own: the old shared id let a shortcut read the wrong field.
- Do not make the note required: a conforming element needs no note.
## Examples
```svelte
```
## Surfaces
- Extension: every wizard.
## Related
Field, AnswerButtons, WizardFooter.
## Decisions
- 2026-09-26: optional by default and said so in the label, since a conforming element needs no note and a required field would slow every answer.
# ConfirmDialog
---
name: ConfirmDialog
slug: confirm-dialog
level: molecule
status: stable
surfaces: []
since: 2026-09
replaces:
- "apps/web: the seven window.confirm() calls and the inline confirmation panel of the extension page"
criteria:
- code: "11.9"
why: "The confirm button is labelled by the caller with a verb that says the effect; the component refuses to render one without a label, so no dialog closes on an OK."
- code: "7.3"
why: "Two real buttons in a dialog form: Enter confirms, Escape cancels, Tab walks between them, and the browser holds the focus inside."
- code: "12.8"
why: "The initial focus lands on the cancel button and the browser returns it to the opener afterwards, so a careless Enter does nothing and the order resumes where it was."
patterns: [destructive-confirmation]
related: [dialog, button]
---
## Purpose
A yes-or-no question before an action, composed from Dialog. The app asks eight of them through `window.confirm()`, which no design reaches, which some browsers let the user silence for a whole site, and whose buttons say "OK" whatever the question. Here the question is the title, the consequence is the body, the button says what it does, and the initial focus is on the least risky answer.
## When to use, when not
Use it before an action the user cannot undo, or would regret: deleting, publishing, closing an audit. Do not use it for an action that can be undone in one step (offer the undo instead), nor as a generic "are you sure", which trains the user to press Enter.
## Anatomy
1. The question, as the dialog's heading.
2. The consequence, as the body: what the action does and what it does not undo.
3. The cancel button, secondary, first and focused.
4. The confirm button, primary or destructive, with the verb of the action.
## States
Plain, and destructive.
## Accessibility contract
- Role and name: Dialog's, named by the question.
- Keyboard, focus: the initial focus is on the cancel button, so Enter pressed by reflex does nothing. Tab reaches the confirm button, then the close button. Escape cancels. The focus returns to the opener.
- Live regions: none. The result of the action, when it must be said, is the page's, through LiveRegion.
## Content rules
French. The title is the question with the verb ("Supprimer l'audit ?"). The body says the consequence in one or two sentences and, when true, "Cette action ne peut pas être annulée." The confirm button repeats the verb ("Supprimer l'audit"); the cancel button says "Annuler".
## Do and don't
- Do set `destructive` when the action deletes or breaks something; the colour says the same thing as the word.
- Do not write "Oui" or "OK" on the confirm button.
- Do not confirm what can be undone: an undo costs the user less than a question.
## Examples
```svelte
Les 106 critères et les captures seront supprimés. Cette action ne peut pas être annulée.
```
## Surfaces
- App: the deletion of an audit, a page or a site, the reopening of an audit, the removal of the second factor, the revocation of every token, the deactivation of an account. No `confirm()` is left in the app.
- Extension: deleting an audit or a page from the panel, when the panel is on Svelte.
## Related
Dialog, Button.
## Decisions
- 2026-09-24: cancel first and focused. The GOV.UK and the site's sheet both put the safe answer under the finger; a destructive default on Enter was the fault the extension's export dialog had.
- 2026-09-24: `confirmLabel` is required, not defaulted, so that a caller has to write the verb.
# ConformityRate
---
name: ConformityRate
slug: conformity-rate
level: molecule
status: stable
surfaces: [app]
since: 2026-08
replaces:
- "apps/web: five renderings of the rate across the dashboard, the site page, the audit page and the report"
criteria:
- code: "3.1"
why: "A symbol (● ◐ ○) says the band; the colour only confirms it."
patterns: []
related: [status-chip, stat]
---
## Purpose
The conformity rate, said one way. It is the central figure of the product and it used to show five ways a few clicks apart. The bands are those of `symboleDuTaux` in `@rgaa-dev/core`, 75 and 50; the middle band stays in the ink because the standard has no "warning" colour and saying otherwise would lie.
## When to use, when not
Use it wherever a rate is published: a page, a site, a campaign. When the audit cannot publish a rate, give `null` and the official sentence; never a rounded or provisional figure.
## Anatomy
1. The symbol of the band, `aria-hidden`.
2. The figure, tabular numerals, with its percent sign.
3. Optionally a suffix ("de conformité").
4. Or, when there is no rate, the reason in the soft ink.
## States
The three bands, the unavailable state with and without a reason, the decimal form for a site average.
## Accessibility contract
- Role and name: text. The figure is a `b`.
- Keyboard, focus, live regions: none.
- The unavailable state is a sentence, never a dash.
## Content rules
Percent with a non-breaking space before the sign, French decimal comma. The unavailable reason comes word for word from the official grid.
## Do and don't
- Do show the sentence when a criterion is still not tested.
- Do not compute a provisional rate and call it a conformity rate.
## Examples
```svelte
```
## Surfaces
- App: the dashboard, the site page, the audit page.
## Related
StatusChip, Stat.
## Decisions
- 2026-08: the thresholds live in core; this component only draws them.
- 2026-09-24: renamed from TauxDeConformite; the band is a modifier class, and the figure takes the decimal comma the doc always promised.
# CriteriaRecap
---
name: CriteriaRecap
slug: criteria-recap
level: molecule
status: stable
surfaces: [extension]
since: 2026-09
replaces:
- "apps/extension: renderCriteriaRecap and renderCriterionBadge, with their inline styles"
criteria:
- code: "3.1"
why: "Each pill carries the status drawing and a hidden word; the ink never says the status alone, and the current pill is underlined, not tinted."
- code: "7.1"
why: "A pill the wizard can jump to is a real button with aria-current; a pill it cannot is plain, so nothing looks pressable and is not."
related: [status-chip, status-counters, wizard-frame, jump-list]
---
## Purpose
The criteria of a theme, each with its status, in one row under a wizard's header, the current one marked and the reachable ones as buttons.
## When to use, when not
Use it in a wizard, for the theme it audits. Not for a page's list of criteria, which is a table.
## Anatomy
1. A list named by the theme.
2. One pill per criterion: the drawing, the number, the hidden word; a button when reachable.
## States
Plain pills, and pills the wizard can jump to with the current one marked.
## Accessibility contract
- Role and name: a list named by `label`; a reachable pill is a `button` named "Aller au critère 8.4, non conforme", the others read "Critère 8.4, conforme"; `aria-current="true"` on the current one.
- Keyboard, focus: the buttons in reading order; the plain pills are not stops.
- Live regions: none.
## Content rules
French; the status words from `@rgaa-dev/core`, in lower case since they read inside the name.
## Do and don't
- Do list only the criteria the wizard judges, in the referential's order.
- Do not make every pill a button: a control that does nothing teaches the auditor to stop pressing.
## Examples
```svelte
```
## Surfaces
- Extension: every wizard's recap.
## Related
StatusChip, StatusCounters, WizardFrame, JumpList.
## Decisions
- 2026-09-26: the drawing comes from the icon set through statusIcon, as the chip's does; the old recap had a fourth table of symbols with two glyphs indistinguishable to the eye.
# DataTable
---
name: DataTable
slug: data-table
level: molecule
status: stable
surfaces: [site]
since: 2026-08
criteria:
- code: "5.4"
why: "The caption is a required prop: a data table has a title that tells it from the others on the page."
- code: "5.6"
why: "Every column header is a th with scope, so every cell knows what it is about."
- code: "12.9"
why: "The table sits in the scroll region component, focusable and named, so a wide one can be scrolled with the keyboard alone."
patterns: [data-table-page]
related: [scroll-region]
---
## Purpose
A data table. A table written by hand almost always misses one of four things, and all four are criteria of theme 5: the caption, the declared headers, a scroll reachable at the keyboard, aligned figures. The component makes them impossible to miss.
## When to use, when not
Use it for a table whose rows are records of the same shape and whose cells are values or small snippets. When cells need row groups, spanning headers or a sticky column, write the table by hand with the base classes inside a scroll region instead.
## Anatomy
1. The scroll region component, framed and named by the caption.
2. A table with a caption, visible or visually hidden.
3. Column headers with `scope="col"`, small capitals; numeric columns right-aligned in tabular numerals.
4. Rows keyed by an identity, cells rendered from the raw value or a snippet.
5. Or, when there are no rows, the empty snippet.
## States
Plain, with a custom cell, empty, caption hidden.
## Accessibility contract
- Role and name: a table named by its caption, inside a region named "caption (zone défilante)".
- Keyboard: Tab reaches the region; a screen reader's table navigation reads headers with each cell.
- Focus: the ring of the base layer around the region.
- No zebra striping: a second background is a second contrast to check, for a gain the hairline already gives.
## Content rules
The caption names the data, not the table ("Critères du thème 6", not "Tableau"). Column headers are nouns.
## Do and don't
- Do mark numeric columns, so figures align and compare.
- Do not hide the caption unless the heading just above says the same thing.
## Examples
```svelte
{#snippet cell({ row, column })}
{#if column.key === 'status'}{:else}{row[column.key]}{/if}
{/snippet}
```
## Surfaces
- Site: the gallery only. The app's tables use the base classes by hand because of their row groups and sticky columns.
## Related
ScrollRegion.
## Decisions
- 2026-08: the caption is required, with no default, so a table cannot be added without naming it.
- 2026-09-24: English props (`caption`, `columns` with `key`, `label`, `numeric`, `rows`, `rowKey`, `cell`, `empty`); the scroll wrapper is the scroll region component rather than a second copy of its markup.
# DefinitionList
---
name: DefinitionList
slug: definition-list
level: molecule
status: stable
surfaces: []
since: 2026-09
replaces:
- "apps/web: the property blocks of the audit, site and admin pages written as divs, once the app is ported"
criteria:
- code: "8.9"
why: "A definition list is used for what it is, pairs of a term and a value, where a table would have nothing to cross and divs would have no structure."
- code: "9.3"
why: "The list is a real dl with dt and dd, so each pair is read as a pair."
related: [table, stat-group]
---
## Purpose
Pairs of a term and its value, as a definition list: the properties of an audit, a site, an account. Two columns when there is room, stacked otherwise; a value can be a string or anything rendered.
## When to use, when not
Use it for the properties of one thing. For rows that share columns, Table; for figures that summarise, StatGroup.
## Anatomy
1. The list.
2. One pair per item: the term in small capitals, the value under it.
## States
Plain, with rendered values, and in one column.
## Accessibility contract
- Role and name: a `dl`; each pair wrapped in a `div`, which the element allows.
- Keyboard, focus: the values' own, when they hold a link.
- Live regions: none.
## Content rules
French. Terms are nouns ("Site audité", "Créé le"); values are the facts, formatted for reading.
## Do and don't
- Do keep one value per term; several values need several pairs or a rendered list.
- Do not use it for a form's summary that the user edits in place; that is a form.
## Examples
```svelte
```
## Surfaces
- App: the account sheet and the profile's address; the report's context block waits for the report document.
## Related
Table, StatGroup.
## Decisions
- 2026-09-24: pairs wrapped in a `div`, the HTML standard's way to group a term and its value for layout.
# Dialog
---
name: Dialog
slug: dialog
level: molecule
status: stable
surfaces: []
since: 2026-09
replaces:
- "apps/site: the modale demo, kept as teaching material; apps/web: the inline confirmation panel of the audit page and the eight confirm() calls, once the app is ported"
- "apps/extension: openPanelModal and its hand-made focus trap, once the panel is on Svelte"
criteria:
- code: "7.1"
why: "The native element carries the dialog role and the name comes from its heading through aria-labelledby; nothing is declared by hand, so nothing can be declared wrong."
- code: "7.3"
why: "showModal holds the focus inside, Escape closes, the close button is a real button: the whole dialog is driven from the keyboard by the browser itself."
- code: "12.8"
why: "When the dialog closes, the browser puts the focus back on the element that opened it, so the tab order resumes where the user left it."
- code: "12.9"
why: "The focus is held inside while the dialog is open, and Escape always releases it: a trap with an exit is not a trap."
patterns: [destructive-confirmation]
related: [confirm-dialog, button, live-region]
---
## Purpose
A modal dialog on the native `dialog` element and nothing else. The element gives, correctly and for free, the four behaviours every hand-made modal gets wrong at least once: the focus is held inside, Escape closes, the page behind is inert, and the focus goes back to the button that opened it. The component adds the name, a labelled close button, a bindable `open` and the value of the button that closed it.
## When to use, when not
Use it for a question the user must answer before going on, a short form, a list to pick from. Do not use it for a message that needs no answer (Alert, or LiveRegion for one that appears after an action), for content that could sit on the page, or for a confirmation of a destructive action, which ConfirmDialog composes from this one.
## Anatomy
1. The heading, which names the dialog.
2. The body.
3. The actions, optional: buttons of type submit inside a `form method="dialog"`, each with a value.
4. The close button, last in the DOM and drawn top right, labelled "Fermer".
5. The backdrop, the one veil the system allows.
## States
Closed, then open as a question or as a form; the outcome is read from the value of the button that closed it.
## Accessibility contract
- Role and name: `dialog`, named by the heading through `aria-labelledby`. The heading is an `h2`; the page's own structure stops at the dialog.
- Keyboard, focus: `showModal` holds Tab inside. The initial focus goes to the first focusable element of the body, or to an element marked `autofocus`; the close button is last in the DOM for that reason. Escape closes. On close, the browser returns the focus to the opener.
- Live regions: none. The outcome, when it must be said, is the page's, through LiveRegion.
- Forced colours: the surface and the line take the system's colours; the backdrop takes Canvas.
## Content rules
French. The title is a question ("Retirer la page de l'audit ?") or a noun phrase ("Renommer l'audit"). An action button says what it does with a verb ("Retirer la page", "Enregistrer"), never "OK" or "Oui". The cancel button says "Annuler".
## Do and don't
- Do bind `open` and set it from the page; never call `showModal` yourself.
- Do give every action button a value: it is what `onclose` hands back.
- Do not put a second dialog inside a dialog.
- Do not open a dialog at page load or after a delay: it takes the focus from wherever the user was.
## Examples
```svelte
```
## Surfaces
- App: the confirmation panel of the audit page and the `confirm()` calls, the page switcher, the export dialog; not ported yet.
- Extension: the page switcher and the export and sync dialogs of the panel, when the panel is on Svelte.
## Related
ConfirmDialog, Button, LiveRegion.
## Decisions
- 2026-09-24: the native element, no focus trap of our own. The site's modale demo proved the four behaviours come from `showModal`; a trap written by hand was the extension's second most patched file.
- 2026-09-24: the close button is last in the DOM. A dialog whose first stop is "Fermer" announces its exit before its question.
- 2026-09-24: the outcome travels as the closing button's value through `onclose`, not as one handler per button, so a confirmation is one function and reads the same whether the user pressed Escape, the close button or "Annuler".
# Disclosure
---
name: Disclosure
slug: disclosure
level: molecule
status: stable
surfaces: []
since: 2026-09
replaces:
- "apps/web: the hand-built criterion rows that fold their detail, once the app is ported"
- "apps/extension: the derogation panel and the folded theme rows of the panel, when it is on Svelte"
criteria:
- code: "7.1"
why: "A native button with aria-expanded and aria-controls: the state and the relation are the ones assistive technology already knows, nothing is invented."
- code: "7.3"
why: "A button toggles on Enter and Space by itself; there is no key handler to get wrong."
- code: "9.1"
why: "The button sits inside the heading, so the title keeps its level in the page's outline and a reader jumps between panels by headings."
- code: "12.8"
why: "A folded panel carries hidden: its links leave the tab order with it, so Tab never lands on content nobody sees."
patterns: []
related: [accordion, tabs]
---
## Purpose
A heading that opens and closes a panel. The accessible version is a native button that says its state and a panel that is really hidden when folded; the component is that and nothing more, with the button inside the heading so the outline stays intact.
## When to use, when not
Use it for a detail the reader may skip: the explanation under a criterion, a derogation's justification, a long list folded by default. Do not use it for content the page needs read (show it), for a set of exclusive panels (Accordion) or for views of equal weight (Tabs).
## Anatomy
1. A heading, at the level the page gives it, holding the button.
2. The button, with the chevron and the title; `aria-expanded` says the state.
3. The panel, `hidden` when folded, named as the button's `aria-controls` target.
## States
Closed, open, and at another heading level.
## Accessibility contract
- Role and name: a button, named by the title; `aria-expanded` for the state, `aria-controls` for the panel. The heading keeps its level.
- Keyboard, focus: Enter and Space toggle. The focus stays on the button. The panel's content is in the tab order only when shown.
- Live regions: none; the state change is announced by the button itself.
- Motion: the chevron turns under motion only.
## Content rules
French. The title is a noun phrase or a question, the same whether the panel is open or closed: never "Afficher" that becomes "Masquer", the state is already said.
## Do and don't
- Do give the heading the level its place in the outline requires.
- Do not open a panel on hover or on focus.
- Do not close the siblings: that is Accordion, and it surprises whoever was reading.
## Examples
```svelte
La page ne contient aucun média temporel.
```
## Surfaces
- App: the criteria of each theme on the audited page, folded under the theme's figures.
- Extension: the derogation panel of the wizards and the theme rows of the sidebar, when the panel is on Svelte.
## Related
Accordion, Tabs.
## Decisions
- 2026-09-24: `hidden` and not a height of zero, as the site's accordéon demo teaches; the demo is the source of this component.
- 2026-09-24: the level is a prop, since a heading at the wrong level is the fault the product finds most on other sites.
# EmptyState
---
name: EmptyState
slug: empty-state
level: molecule
status: stable
surfaces: []
since: 2026-09
replaces:
- "apps/web: the soft paragraphs under empty lists of the lab, the projects and the admin pages, once the app is ported"
criteria:
- code: "9.1"
why: "The message is a heading at the level the page gives, so a reader jumping by headings finds the empty list where the list would be."
- code: "11.9"
why: "The action, when there is one, is a Button with a verb; a link that says 'ici' has no place here."
patterns: [empty-state]
related: [button, data-table]
---
## Purpose
What a list shows when it has nothing to show: a heading that says what is missing, a sentence that says what to do, and the action that does it when there is one.
## When to use, when not
Use it in place of an empty table or list, whether the user has nothing yet or a search found nothing. Not for an error (Alert), not for a loading state (BusyRegion).
## Anatomy
1. An optional drawing, hidden.
2. The heading, at the level the page gives.
3. The text.
4. The action.
## States
With an action, without, and with a drawing.
## Accessibility contract
- Role and name: a heading and text; the action is a Button.
- Keyboard, focus: the action's.
- Live regions: none. A search result that changes in place is announced by the page's LiveRegion, not by this block.
## Content rules
French. The title starts with "Aucun" or "Aucune" and says what: "Aucun audit pour l'instant.", "Aucune capture pour ce critère." The text says what to do next, in one or two sentences.
## Do and don't
- Do give `level` the level the list's own heading would have had.
- Do not show an empty state while the list is loading.
## Examples
```svelte
{#if audits.length === 0}
Ajoutez la première page et l'extension fera le reste.
{#snippet action()}{/snippet}
{/if}
```
## Surfaces
- App: the dashboard's search, a site without audits, an organisation without sites, an audit without pages, an account without tokens.
- Extension: the audit list of the panel, when it is on Svelte.
## Related
Button, DataTable.
## Decisions
- 2026-09-24: a heading, not a paragraph, so the empty list is in the outline where the list would be.
# ErrorSummary
---
name: ErrorSummary
slug: error-summary
level: molecule
status: stable
surfaces: []
since: 2026-09
replaces:
- "apps/site: the récapitulatif-d-erreurs demo, kept as teaching material"
- "apps/web: the error paragraphs at the top of the auth and settings forms, once the app is ported"
criteria:
- code: "11.10"
why: "The summary is how the form reports its input control: every error is listed, tied to its field, and the focus goes to the list at submission."
- code: "11.11"
why: "The entries repeat the field messages, which say what to write, so the correction is suggested where the errors are counted."
- code: "7.5"
why: "The summary takes the focus, so it is not a status message and carries neither role alert nor aria-live; announcing it twice would be the fault."
- code: "12.8"
why: "Each entry is a link that puts the focus on the field at fault, so the correction path follows the reading order."
patterns: [form-with-error-summary]
related: [field, live-region, alert]
---
## Purpose
The list of a form's errors at its top, focused at submission, each entry a link to the field at fault. The pattern is GOV.UK's: the summary says how many errors there are and where, the inline message says what to correct, where one corrects.
## When to use, when not
Use it on every form with more than one field that can fail. Do not use it for a single field's error (Field shows it), for a success (LiveRegion), or as a live region: it takes the focus, so it is not a status message.
## Anatomy
1. The heading, with the count, which takes the focus.
2. The list, one link per error, with the field's message.
3. The line of non-conformity around, no fill.
## States
A form that submits: no error, two errors, one error, saved.
## Accessibility contract
- Role and name: a heading at the level given, inside a plain block. No `role="alert"`, no `aria-live`.
- Keyboard, focus: the heading takes the focus when the list is set; it is never a Tab stop. Each entry is a link; Enter moves the focus to the field.
- Live regions: none, by design.
## Content rules
French. The heading counts: "Il y a un problème", "Il y a 3 problèmes". Each entry is the field's message, which says what to write and gives an example (criterion 11.11).
## Do and don't
- Do set `errors` on submit and only then; the summary takes the focus each time.
- Do give each error the id of its control, the one Field puts on the input.
- Do not add a live region around it, nor a role.
- Do not validate on each keystroke into the summary.
## Examples
```svelte
```
## Surfaces
- App: every form of the auth pages and the settings; not ported yet.
## Related
Field, LiveRegion, Alert.
## Decisions
- 2026-09-24: the focus goes to the heading and the link replays the focus move, as the site's demo does, since a router that intercepts internal links hides what the anchor does alone.
# Field
---
name: Field
slug: field
level: molecule
status: stable
surfaces: [site, app]
since: 2026-08
replaces:
- "apps/web: sixteen hand-built label and input pairs with help text in three sizes"
criteria:
- code: "11.1"
why: "for and id come from the same prop, so a label cannot be left unassociated."
- code: "11.2"
why: "The label is visible and never replaced by a placeholder, which disappears at the first keystroke."
- code: "11.10"
why: "Required is in the accessible name and in the attribute; the error is tied to the field with aria-describedby and aria-invalid."
- code: "11.13"
why: "autocomplete is typed as the closed list of the standard, so a typo cannot silently switch autofill off."
patterns: [form-with-error-summary]
related: [button, alert, icon]
---
## Purpose
A form field, label included. It exists to make impossible four faults the RGAA sanctions and that a hand-written field repeats endlessly: an unassociated label, a required mark by colour or asterisk only, an error not tied to its field, a help text shown but never announced.
## When to use, when not
Use it for every text-like input: text, email, password, url, number, tel, search, and multi-line text. For a choice among options use native radios, checkboxes or a select with their own components; for a search form with a button, the search field component.
## Anatomy
1. The label, visible, with the required mark: an asterisk hidden from assistive technology and the word "(obligatoire)" visually hidden.
2. The hint, announced with the field.
3. The input or the textarea, with `aria-describedby` listing the error then the hint, and `aria-invalid` when there is an error.
4. The error, with the cross icon and the message.
## States
Text, email, password, required, with hint, in error, multi-line.
## Accessibility contract
- Role and name: a textbox named by its label, "(obligatoire)" included when required.
- Description: the error first, then the hint, so what blocks is heard first.
- Keyboard: native.
- Focus: the ring of the base layer; the field's border turns to the action colour.
- Live regions: the error carries `role="alert"` so that a correction is announced without moving the focus.
## Content rules
Labels are nouns ("Adresse électronique"), hints are sentences that say the format, errors say what to do ("Indiquez une adresse avec un @").
## Do and don't
- Do give a hint for any format the user could not guess.
- Do not rely on the placeholder for anything the user must keep in mind.
## Examples
```svelte
```
## Surfaces
- Site: the gallery; the contact and beta forms still write their fields by hand.
- App: every form, from sign-in to the report's parameters; no label or input is written by hand any more.
## Related
Button, Alert, Icon.
## Decisions
- 2026-08: the error is announced before the hint, because it is what blocks.
- 2026-09-24: English props (`required`, `hint`, `error`, `multiline`, `rows`); the error's symbol is the cross icon; the control's height comes from padding and font, never from a pixel value, so zoomed text zooms the field.
# FilterGroup
---
name: FilterGroup
slug: filter-group
level: molecule
status: stable
surfaces: [app]
since: 2026-09
replaces:
- "apps/web: the row of status filter buttons above the audits list, written as links with a current class"
criteria:
- code: "7.1"
why: "Native buttons with aria-pressed in a group named by its visible label: the state and the group are the ones assistive technology knows."
- code: "3.1"
why: "A pressed filter is told by aria-pressed and the fill with the bold, not by a hue on an outline."
- code: "11.1"
why: "The group's label is visible and tied by aria-labelledby, so the filters are heard as 'Statut, groupe'."
patterns: [filter-group]
related: [radio-group, select, live-region, chip]
---
## Purpose
A row of filters that apply the moment they are pressed: a group named by a visible label, one button per filter with `aria-pressed`, several pressed at once or one alone when exclusive. No form, no "Appliquer"; the page says the count through its live region.
## When to use, when not
Use it above a list for two to six filters the user sees at once. Use a RadioGroup when the filters are part of a form submitted later, a Select when there are more than six. Not for navigation between pages, which is a list of links.
## Anatomy
1. The visible label.
2. The buttons, each with its word and its count when given.
## States
Exclusive with "Tous" first, and several at once.
## Accessibility contract
- Role and name: a `group` named by the label through `aria-labelledby`; each button named by its word, with `aria-pressed`.
- Keyboard, focus: Tab reaches each button; Enter and Space press it; the focus never moves on a press.
- Live regions: none of the group's. The page says the count of the filtered list through LiveRegion, present from the first render.
## Content rules
French. The label is a noun ("Statut", "Thématiques"); the filters are nouns or participles in the plural ("Terminés", "Brouillons"); the first exclusive filter is "Tous".
## Do and don't
- Do announce the count after a press, in a sentence: "5 audits affichés".
- Do list "Tous" first when exclusive, so the list is never filtered by nothing.
- Do not move the focus to the list on a press; the user is still choosing.
## Examples
```svelte
```
## Surfaces
- App: the audits list, the users list.
## Related
RadioGroup, Select, LiveRegion, Chip.
## Decisions
- 2026-09-26: buttons with `aria-pressed` and not radios: the filters apply at once, and a radio group promises a form.
- 2026-09-26: an exclusive group refuses to unpress its last filter: a list filtered by nothing is the whole list, which "Tous" already says.
# JumpList
---
name: JumpList
slug: jump-list
level: molecule
status: stable
surfaces: [extension]
since: 2026-09
replaces:
- "apps/extension: renderListeSaut, the jump list of the resume module"
criteria:
- code: "7.1"
why: "One real button per element, named by its rank, its label and its status, with aria-current on the current one."
- code: "12.8"
why: "Collapsed by default under a disclosure, the list never separates the question from its answers in the tab order."
- code: "3.1"
why: "The status is in the button's name and drawn by the icon; the current element is a line and the bold, not a hue."
related: [disclosure, criteria-recap, resume-banner]
---
## Purpose
The elements of a wizard, to jump to one already seen: a disclosure that says how many there are and how many remain, and one button per element.
## When to use, when not
Use it in a wizard with several elements, under the header. Not for a list of one, and not for the criteria, which is CriteriaRecap.
## Anatomy
1. The disclosure, titled by the summary.
2. An ordered list of buttons: the drawing, the rank, the label.
## States
Closed, and open with the current element and every status.
## Accessibility contract
- Role and name: the disclosure's button names the list; each item is a `button` named "3. Photo du produit, non conforme", "élément courant" added on the current one, which also carries `aria-current="true"`.
- Keyboard, focus: the disclosure's button, then the items when open. The list is marked `data-raccourcis="off"`: the wizard's shortcut listener leaves arrow keys alone inside it.
- Live regions: none.
## Content rules
French. The summary counts elements and what remains ("12 éléments, 3 sans réponse"); the labels are the elements' own words, cut by the ellipsis on screen and whole in the name.
## Do and don't
- Do keep it closed by default: open, it buried the question under a long list.
- Do not put the jump list in the footer; it sits under the header, with the recap.
## Examples
```svelte
```
## Surfaces
- Extension: every inventory wizard.
## Related
Disclosure, CriteriaRecap, ResumeBanner.
## Decisions
- 2026-09-26: on Disclosure rather than a bare details element, so the button and the panel are the package's; the `data-raccourcis="off"` mark is the wizard shortcut contract, documented on `use:shortcuts`.
# Menu
---
name: Menu
slug: menu
level: molecule
status: stable
surfaces: []
since: 2026-09
replaces:
- "apps/web: lib/components/MenuActions.svelte and the row menu that used it, gone"
- "apps/site: the bouton-menu demo, kept as teaching material"
criteria:
- code: "7.1"
why: "The button says it opens a menu with aria-haspopup and aria-expanded; the menu carries role menu with a name, the entries role menuitem in items with role none."
- code: "7.3"
why: "The arrows, Home, End and Escape are handled; the first letters find an entry; the focus returns to the button when the menu closes."
- code: "12.8"
why: "One entry is in the tab order at a time, so the group costs one Tab stop, and Tab leaves the menu without taking the focus back."
- code: "12.10"
why: "The first-letter search is a single-key shortcut bound to the open menu with the focus inside it, and to nothing else."
patterns: [data-table-page]
related: [icon-button, disclosure, button]
---
## Purpose
A menu of commands behind a button. The rule: does the entry lead somewhere, or does it do something here? A list of links is a Disclosure; commands are what this component holds, with what the ARIA pattern promises at the keyboard and one Tab stop for the group.
## When to use, when not
Use it for the commands of a row or an object: duplicate, export, close, delete. Not for links (a Disclosure with a list of links), not for one command (a Button), not for a choice that is a value (Select).
## Anatomy
1. The button, with its text and a chevron, or three dots named by the label.
2. The menu, named, aligned to the start or the end of the button.
3. The entries, one command each; a destructive one in the red of non-conformity.
## States
Three dots, a labelled button, and an unavailable command.
## Accessibility contract
- Role and name: a button with `aria-haspopup`, `aria-expanded` and, while open, `aria-controls`; a `menu` named by `label`; `menuitem` entries in `role="none"` items.
- Keyboard, focus: Enter, Space and the down arrow open on the first entry, the up arrow on the last. Down, Up, Home and End move, wrapping; a letter moves to the next entry that starts with it; Escape closes and returns the focus; Tab closes and lets the focus go; a choice closes, returns the focus, then calls `onselect`.
- Pointer: a click outside closes; no veil, which would cost a second click.
- Live regions: none. The outcome of a command is the page's, through LiveRegion.
## Content rules
French. Entries are verbs with their object: "Exporter en HTML", "Supprimer l'audit". The label names what the commands act on: "Actions pour camille@exemple.fr".
## Do and don't
- Do give `label` a name that says which object, when several rows have a menu.
- Do not put links in it; that is a Disclosure.
- Do not confirm inside the menu: a destructive entry opens a ConfirmDialog after the menu closed.
## Examples
```svelte