Foundations
Accessibility
Accessibility is built into the components rather than audited onto them. The target is WCAG 2.2 AA, and the way it is met is by making the accessible path the easiest one to take.
Enforced by the API
IconButton will not compile without a label. DialogTitle is required. FormField builds the aria-describedby chain for you. Where a rule can be made structural, it has been — because a rule that depends on remembering will eventually be forgotten.
Focus visibilityPermalink to this section
One focus treatment, applied everywhere: a 2px brand outline offset by 2px. It is never removed for mouse users, and it clears 4.6:1 against the light canvas.
Tab through these to see the real focus ring.
@layer base {
/* :focus-visible, not :focus — the ring appears for keyboard
navigation and stays out of the way for pointer users,
without ever being removed entirely. */
:focus-visible {
outline: 2px solid var(--border-focus);
outline-offset: 2px;
border-radius: 3px;
}
}ContrastPermalink to this section
Every text and interface colour in the system was chosen against the surface it actually appears on.
| Requirement | Minimum | System | Status |
|---|---|---|---|
| Body text | 4.5:1 | 17.5:1 — foreground on background | |
| Supporting text | 4.5:1 | 5.8:1 — muted-foreground on background | |
| Button label | 4.5:1 | 4.6:1 — white on brand | |
| Focus indicator | 3:1 | 4.6:1 — brand on background | |
| Error text | 4.5:1 | 5.4:1 — danger on background | |
| Placeholder | 4.5:1 | 3.1:1 — faint-foreground |
The placeholder is the one deliberate exception. It never carries information a person needs, because the label and description are always present — which is why the system forbids placeholder-as-label rather than darkening the colour.
Colour is never the only signalPermalink to this section
Roughly one in twelve men and one in two hundred women has a colour-vision difference. Every state in the system is carried by at least two of colour, shape, icon and text.
Enter a complete address, like ana@borneo.design.
Do — pair colour with a word and an icon
The red border, the alert icon and the message all say the same thing. Any one of them is enough.
Don't — signal with colour alone
A red border with no message says something is wrong but not what — and says nothing at all in greyscale.
KeyboardPermalink to this section
Everything the mouse can do, the keyboard can do. Complex widgets use Radix primitives specifically because rebuilding their focus behaviour by hand goes wrong.
| Surface | Behaviour |
|---|---|
| Skip link | First tab stop on every page, jumping to #main-content |
| Navbar | Standard tab order; the search button opens the command menu |
| Command menu | ⌘K / Ctrl+K, arrows to move, Enter to open, Escape to close with focus restored |
| Sidebar | Group headers are buttons with aria-expanded; links are ordinary tab stops |
| Tabs | Arrow keys move and select; one Tab enters the row, one leaves it |
| Dropdown menu | Arrows, typeahead, submenus on ← and →, Escape returns focus to the trigger |
| Dialog | Focus trapped, Escape closes, focus returns to whatever opened it |
| Tooltip | Opens on focus, and Escape dismisses it without moving focus |
| Code blocks | The scroll region is focusable, so wide snippets can be read by keyboard |
Screen reader labellingPermalink to this section
Icon-only controls, status changes and form errors are the three places where a visual-only implementation silently excludes people.
// 1. Icon-only controls carry their name in the API,
// so it cannot be forgotten.
<IconButton label="Delete project" icon={<Trash2 />} />
// 2. Changes that happen without a page navigation are
// announced through a live region.
<p role="status" aria-live="polite">
{shown} of {total} projects
</p>
// 3. Errors appearing after a submit use role="alert",
// which interrupts — appropriate for a failed action.
<p id="email-message" role="alert" className="text-danger">
Enter a complete address, like ana@borneo.design.
</p>
// 4. Decorative icons are hidden so they are not
// announced alongside the label they duplicate.
<Plus aria-hidden />Touch targetsPermalink to this section
WCAG 2.2 requires 24×24px; this system targets 44×44px on coarse pointers, which is the size a thumb actually needs.
/* Every interactive component carries data-control, so the
minimum is applied in one place rather than per component.
The 36px small size still meets 44px on touch. */
@media (pointer: coarse) {
[data-control] {
min-height: 44px;
min-width: 44px;
}
}Small controls that cannot grow — a checkbox, a switch — are paired with a label that forms part of the target. That is why CheckboxField and SwitchField exist rather than being left to each caller.
Reduced motionPermalink to this section
Movement is removed rather than shortened. A fast slide is still a slide.
The global media query strips animations and transforms while keeping opacity and colour transitions, so state changes stay perceivable without anything travelling across the screen. Components that animate through JavaScript read the same preference through the usePrefersReducedMotion hook and drop their transform values. See the motion foundation for a live demonstration.
What is checked before a component shipsPermalink to this section
This is the list used in review. A component that fails any line does not ship.
- Semantic HTML — a button is a <button>, a link with an href is an <a>.
- Heading order is sequential; no level is skipped for visual reasons.
- Every interactive element is reachable and operable by keyboard alone.
- Focus is visible at every stop, and focus order matches visual order.
- Icon-only controls have an accessible name.
- Form controls have associated labels; help and error text are linked with aria-describedby.
- Colour is never the sole carrier of meaning.
- Text meets 4.5:1; interface boundaries and focus indicators meet 3:1.
- Touch targets reach 44×44px on coarse pointers.
- Animations respect prefers-reduced-motion by removing movement.
- Dialogs and menus trap focus while open and restore it on close.
- Content that updates without navigation is announced through a live region.
What automated testing will not catchPermalink to this section
Automated tools find roughly a third of accessibility problems. These are the ones that need a person.
- Whether a label actually describes what the control does, rather than merely existing.
- Whether the focus order makes sense as a sequence, not just whether every element is reachable.
- Whether an error message explains how to fix the problem.
- Whether an alternative to a colour-coded state is genuinely equivalent, or a token gesture.
- Whether a live region announces at a useful moment, rather than interrupting constantly.
- Whether the interface is usable at 200% zoom with a 320px viewport.