Accessibility

Accessibility is not a feature — it's a quality bar. HTML is the accessible baseline. Browsers provide accessibility for free if you use semantic elements.

Semantic HTML first

If you use a <div> for a button, you have to rebuild 20 years of browser engineering yourself. Use the right element and get keyboard, focus, and screen reader support for free.

Use native elements

Native elements carry semantics, keyboard behaviour, and ARIA roles automatically.

<button onClick={handleSave}>Save</button>
<a href="/settings">Settings</a>
<nav aria-label="Main"></nav>
<main></main>

Do

Use <button> for actions, <a> for navigation, <nav>, <main>, <article> for landmarks.

<div onClick={handleSave} className="btn">Save</div>
<span onClick={() => navigate('/settings')}>Settings</span>

Don’t

Use <div> or <span> with click handlers to create custom buttons or links. You lose keyboard support, focus management, and screen reader announcements.

Meaningful document structure

Heading hierarchy is the table of contents for screen reader users.

Do

Use headings in order (<h1> <h2><h3>). Don't skip levels. Every page should have exactly one <h1>.

Don’t

Choose heading levels based on visual size. Use <h4> after <h2> because it looks right — use text utility classes for visual sizing instead.

ARIA as a last resort

The first rule of ARIA is don't use ARIA — if a native HTML element with the semantics you need exists, use it. ARIA supplements HTML; it doesn't replace it.

Prefer native over ARIA

ARIA overrides native semantics. Used incorrectly, it makes things worse, not better.

<!-- Native checkbox — keyboard, focus, state for free -->
<input type="checkbox" id="terms" />
<label htmlFor="terms">I agree to the terms</label>

Do

Reach for ARIA only when there is no native element for the pattern (e.g., tabs, tree views, comboboxes). Roadie's Base UI components handle ARIA attributes automatically for these cases.

<!-- Reinventing a checkbox from scratch -->
<div role="checkbox" aria-checked="false"
tabIndex={0} onClick={toggle}
onKeyDown={handleKeyDown}>
I agree to the terms
</div>

Don’t

Build custom ARIA widgets when a native element exists. Every attribute you add manually is an attribute you must maintain.

Color and contrast

Sufficient contrast is non-negotiable. Never convey meaning through colour alone. See Colors for the full colour system.

Meet WCAG AA contrast ratios

4.5:1 for normal text. 3:1 for large text (18px bold / 24px regular) and UI components.

Do

Use Roadie's semantic text colours (text-normal, text-subtle, text-strong) which are designed to meet contrast ratios in both light and dark modes.

Don’t

Use raw colour values without checking contrast. Light grey text on a white background may look subtle but fails WCAG.

Colour is not the only signal

Colour-blind users cannot distinguish red from green. Always pair colour with another indicator.

Do

Pair colour with icons, text labels, or patterns. An error state uses intent-danger (red) plus an error icon plus error text.

Don’t

Indicate success or failure only through green/red colouring with no supporting text or icon.

Keyboard and focus

Keyboard accessibility is covered in depth in the Interactions foundation — including keyboard operability, visible focus indicators, focus management, and hit targets.

Reduced motion

Motion can cause nausea and disorientation. Roadie includes a global prefers-reduced-motion reset. See Motion for full details on duration tokens, easing, and how to test.

Screen readers

Screen readers interpret the DOM, not the pixels. What you see is not what they hear.

Meaningful alt text

Images without alt text are invisible to screen readers. Decorative images need empty alt.

<!-- Informative image -->
<img src="seating-map.png" alt="Venue seating map showing sections A through F" />
<!-- Decorative image -->
<img src="divider.svg" alt="" />

Do

Write alt text that describes the content or function of the image, not its appearance. Use empty alt="" for purely decorative images.

Don’t

Omit the alt attribute entirely (screen readers will read the filename) or write “image” as alt text.

Announce dynamic content

Screen readers don’t automatically notice DOM changes. Tell them what changed.

<!-- Announce status updates -->
<div role="status" aria-live="polite">
3 items added to cart
</div>
<!-- Announce urgent errors -->
<div role="alert">
Payment failed. Please try again.
</div>

Do

Use aria-live="polite" for non-urgent updates (status messages, search results). Use role="alert" for urgent notifications (errors, warnings).

Don’t

Update content silently. A toast notification that isn't announced leaves screen reader users unaware of what happened.

Forms

Form interaction patterns (labels, placeholders, validation, submit behaviour) are covered in Interactions. This section covers the ARIA-specific attributes that make forms accessible to assistive technology.

Associate errors with fields

Screen readers need a programmatic link between an input and its error message.

<label htmlFor="email">Email</label>
<input
id="email"
type="email"
aria-invalid={hasError}
aria-describedby={hasError ? "email-error" : undefined}
/>
{hasError && (
<p id="email-error" role="alert">
Enter a valid email address
</p>
)}

Do

Use aria-describedby to link error messages to their field. Use aria-invalid to indicate the field has an error.

Don’t

Show a red border and error text next to a field without any programmatic association — sighted users see it, screen reader users don't.

Group related fields

Field groups need a shared label so screen readers announce the context.

<fieldset>
<legend>Billing address</legend>
<label htmlFor="street">Street</label>
<input id="street" />
<label htmlFor="city">City</label>
<input id="city" />
</fieldset>

Do

Use <fieldset> and <legend> to group related fields. Screen readers announce the legend before each field in the group.

Don’t

Group fields under a visual heading with no programmatic relationship. Screen reader users won't know the fields are related.

What Roadie handles

The design system provides accessible primitives. You get these for free when using Roadie components.

Base UI primitives

Components built on Base UI provide correct ARIA attributes, focus management, and keyboard navigation out of the box.

Focus rings

is-interactive provides visible :focus-visible rings coloured by the nearest intent.

Field state styling

is-interactive-field provides visual state transitions for focus and aria-invalid automatically.

Contrast-safe tokens

Semantic colour tokens (text-normal, text-subtle) are designed to maintain contrast ratios in both light and dark modes.

Testing checklist

Run through this checklist before shipping. Automated tools catch about 30% of issues — the rest requires manual testing.

TestHowWhat to check
KeyboardTab through the pageEvery interactive element reachable and operable. Focus order is logical. No keyboard traps.
Screen readerVoiceOver (macOS) or NVDA (Windows)Content is announced meaningfully. Dynamic updates are heard. Form errors are associated.
ContrastBrowser DevTools or contrast checkerAll text meets WCAG AA. UI components meet 3:1.
Reduced motionDevTools → Rendering → Emulate prefers-reduced-motionNo animations play. No functionality is lost.
ZoomBrowser zoom to 200%No content is clipped or overlapping. Layout reflows gracefully.