Skip to main content
ACCESSIBILITY

Building a11y first

Draft

Build 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 Enter or Space?
  • 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> or aria-label?
  • Focus visible — Does every interactive element show :focus-visible using var(--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;
}