Building a11y first
DraftBuild accessibility in from the start — it costs 10x less than retrofitting and eliminates entire categories of rework.
The cost of retrofitting
When accessibility is treated as a final checkpoint, teams encounter predictable problems:
- Structural rewrites — a custom dropdown built with
<div>elements needs to be rebuilt with proper<select>or listbox semantics - Focus order chaos — visually reordered layouts that do not match DOM order confuse keyboard users
- Missing announcements — dynamic content changes that were never wired to ARIA live regions
- Lost sprint time — accessibility bugs filed late in the cycle compete with feature work
Building a11y first eliminates these categories of rework entirely.
The a11y-first process
1. Start with the interaction model
Before opening a design tool, define how the component will be operated:
- What happens when a keyboard user presses
Tab?Enter?Escape? Arrow keys? - What does a screen reader announce when the component receives focus?
- What changes when the component's state updates — and how is that change communicated?
Write these answers down. They are the accessibility specification, and they should exist before any visual design begins.
2. Choose the right HTML element
Select the semantic HTML element that matches your interaction model. If you need a button, use <button>. If you need a link, use <a>. If you need a group of mutually exclusive options, use <fieldset> with <input type="radio">.
The right element gives you keyboard behaviour, focus management, and screen reader semantics without writing a single line of JavaScript.
<!-- Interaction: user selects one option from a group -->
<!-- Right choice: radio group -->
<fieldset>
<legend>Shipping speed</legend>
<label><input type="radio" name="shipping" value="standard" /> Standard (5-7 days)</label>
<label><input type="radio" name="shipping" value="express" /> Express (2-3 days)</label>
<label><input type="radio" name="shipping" value="overnight" /> Overnight</label>
</fieldset>
<!-- Wrong choice: clickable divs -->
<div>
<div className="option selected" onClick={...}>Standard</div>
<div className="option" onClick={...}>Express</div>
<div className="option" onClick={...}>Overnight</div>
</div>
3. Layer ARIA only when HTML falls short
There are cases where native HTML does not provide the semantics you need — tabs, comboboxes, tree views, and other composite widgets. In those cases, use ARIA roles, states, and properties to fill the gap.
The rule is: add ARIA to describe what you have built, never to replace what HTML already provides.
// A tab interface has no native HTML equivalent, so ARIA is appropriate
<div role="tablist" aria-label="Account settings">
<button role="tab" aria-selected={activeTab === 'profile'} aria-controls="panel-profile">
Profile
</button>
<button role="tab" aria-selected={activeTab === 'security'} aria-controls="panel-security">
Security
</button>
</div>
<div role="tabpanel" id="panel-profile" aria-labelledby="tab-profile">
{/* panel content */}
</div>
4. Test with the keyboard before the mouse
After implementing a component, put your mouse away. Navigate through the entire interaction using only the keyboard:
- Can you reach every interactive element with
Tab? - Can you activate buttons and links with
EnterorSpace? - Can you dismiss overlays with
Escape? - Is the focus indicator always visible?
- Does focus move logically through the interface?
If any of these fail, fix them before moving on to visual polish.
5. Verify with a screen reader
Turn on VoiceOver (macOS: Cmd + F5) or NVDA (Windows) and navigate the component:
- Does the screen reader announce the element's role, name, and state?
- When state changes (e.g., a checkbox is checked), is the change announced?
- Are dynamic updates — loading states, error messages, success confirmations — announced via live regions?
See the Screen Readers page for detailed testing instructions.
Understanding disability
Building a11y first requires understanding that disability is not a narrow category. The spectrum includes:
Permanent disabilities
People who are blind, deaf, or have motor impairments that are lifelong conditions. These users rely on assistive technology daily.
Temporary disabilities
A broken arm, an eye infection, or a migraine. These conditions are short-lived but create the same barriers as permanent disabilities during their duration.
Situational disabilities
Bright sunlight washing out a screen, holding a baby in one arm, or working in a noisy environment. These are context-dependent and affect everyone at some point.
Designing for permanent disabilities creates benefits that cascade to temporary and situational users. Captions help deaf users, but they also help someone in a loud airport. High contrast helps low-vision users, but it also helps someone using their phone outdoors.
Design checklist
Use this checklist during design reviews to catch accessibility issues before implementation:
- Color contrast — Does all text meet 4.5:1 (normal) or 3:1 (large) contrast ratios?
- Color independence — Is information conveyed through means other than color alone?
- Touch targets — Are all interactive elements at least 44x44px?
- Text alternatives — Do all non-text elements have appropriate alt text or are marked decorative?
- Heading hierarchy — Do headings follow a logical order (h1, h2, h3) without skipping levels?
- Error identification — Are form errors described in text, not just indicated by color?
- Motion — Can animations be paused or disabled? Do they respect
prefers-reduced-motion? - Keyboard flow — Does the visual layout match the expected tab order?
Development checklist
Use this checklist during code review:
- Semantic HTML — Are native elements used where possible (
<button>,<a>,<input>)? - Labels — Does every form control have a visible
<label>oraria-label? - Focus visible — Does every interactive element show
:focus-visibleusingvar(--color-border-focus)? - Focus management — Do modals trap focus? Does focus return on close?
- ARIA correctness — Are ARIA roles, states, and properties used correctly (not redundantly)?
- Live regions — Are dynamic content changes announced via
aria-live? - Zoom — Does the layout work at 200% browser zoom without horizontal scrolling?
- axe-core — Does the component pass automated accessibility checks with zero violations?
Common mistakes
Using aria-label when a visible label exists
If a form field already has a <label> element, adding aria-label overrides the visible label for screen reader users. This creates a disconnect between what sighted and non-sighted users perceive.
Disabling focus outlines globally
/* Never do this */
*:focus { outline: none; }
This removes the only way keyboard users can track their position on the page. Arch UI's var(--color-border-focus) provides a consistent, visible focus ring that should never be overridden.
Using tabIndex values greater than 0
Positive tabIndex values create an unpredictable focus order that is nearly impossible to maintain as the page grows. Use tabIndex={0} to add an element to the natural tab order, or tabIndex={-1} to make it programmatically focusable without being in the tab sequence.
Hiding content with display: none when it should be accessible
Content hidden with display: none is removed from both the visual display and the accessibility tree. If content should be visually hidden but still announced by screen readers, use a visually-hidden utility class instead.
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}