Accessibility Is Not a Feature. It Is the Work.

Introduction

I have worked on many projects over the years where accessibility was treated as a separate phase — something that happened after the real work was done, usually under pressure from a client who had received a compliance report or a legal requirement. The process was always the same: an audit, a list of violations, a sprint to fix the most visible ones, and then a return to building features without accessibility in mind until the next audit.

I was part of that process on multiple projects before I understood why it was wrong — not just ethically wrong, but practically wrong. The accessibility problems that are found in audits are almost always structural. They are the result of months of decisions made without accessibility in mind, and fixing them at the end requires changing the structure that all the subsequent work was built on top of. It is expensive, it is incomplete, and it does not stick because the team’s habits did not change along with the fixes.

I spent two months retrofitting accessibility into an existing Angular healthcare dashboard, and I learned more about accessibility in those two months than in everything that came before — because I had to understand why each thing was broken before I could fix it properly. The fixes were not hard once I understood the mechanisms. What was hard was that every fix revealed a deeper structural problem that the fix alone could not address.

What Accessibility Actually Is

The definition most commonly given is: making your application usable by people with disabilities. That definition is true but it is incomplete in a way that leads to misunderstanding.

A more complete definition: accessibility is the practice of building applications that work across the full range of human variability — in vision, hearing, motor ability, cognitive function, and the technology people use to interact with what you build.

The distinction matters because “disability” is often heard as a fixed category of people who are separate from the typical user. But the spectrum of human variability is continuous, contextual, and affects everyone at different times. A person with a broken arm has a temporary motor impairment. A person holding a baby has one hand available. A person in bright sunlight cannot see their screen clearly. A person in a noisy environment cannot rely on audio cues. A person who is stressed or tired has reduced cognitive capacity. A person using a keyboard because their mouse broke has a situational input constraint.

The World Health Organisation estimates that over one billion people — roughly 15% of the global population — live with some form of disability. In the context of any large application, that is not a niche audience. And when you add situational and temporary impairments, the proportion of users who benefit from accessible design at any given moment is significantly higher.

Accessibility is not about building a special version of your application for a minority. It is about building one version of your application that works for everyone.

The Types of Accessibility — What the Full Spectrum Looks Like

Understanding the categories helps you understand the different mechanisms involved, because different impairments require different solutions.

Visual accessibility

Visual impairments range from complete blindness to low vision, color blindness, light sensitivity, and age-related vision changes.

Blind users navigate with screen readers — software that reads the page aloud and allows navigation through keyboard commands. VoiceOver on macOS and iOS, NVDA and JAWS on Windows, TalkBack on Android are the most common. A screen reader cannot interpret images, icons, or visual layout — it only processes what is in the DOM and what the browser’s accessibility tree exposes.

Low vision users may use screen magnification, high contrast modes, or custom font sizes. They often benefit from the same things that improve readability generally: sufficient contrast ratios, scalable text, layouts that do not break when text is enlarged.

Color blind users cannot rely on color alone to distinguish information. Roughly 8% of men and 0.5% of women have some form of color blindness. Status indicators, validation states, charts, and alerts that communicate through color alone are inaccessible to this population.

Motor accessibility

Motor impairments range from paralysis to tremors, limited dexterity, and fatigue-related conditions. Users with motor impairments may navigate entirely by keyboard, by switch access, by eye tracking, or by voice control software like Dragon NaturallySpeaking.

The primary concern for motor accessibility is keyboard operability — every interaction in the application must be reachable and operable without a pointing device. This includes focus management (knowing where focus is and moving it intentionally), logical tab order (the sequence in which focusable elements receive focus), and sufficient target sizes (buttons and links large enough to activate without precise pointing).

Auditory accessibility

Auditory impairments affect users who cannot hear audio content — including video narration, audio alerts, and voice-based interfaces.

The primary concern is providing text alternatives for audio content: captions for video, transcripts for audio, visual alternatives for audio alerts.

Cognitive accessibility

Cognitive accessibility is the most underserved dimension of accessibility and the one that benefits the broadest population. Cognitive impairments include dyslexia, ADHD, autism spectrum conditions, memory impairments, and acquired cognitive disabilities from injury or illness.

The design patterns that support cognitive accessibility are also the patterns that make applications easier to use for everyone: clear and consistent navigation, simple language, predictable interactions, sufficient reading time, reduced cognitive load, and recovery paths for errors. Cognitive accessibility is where accessibility and general UX quality overlap most directly.

The Standards: WCAG — What They Are and Why They Matter

The Web Content Accessibility Guidelines (WCAG) are the internationally recognized standard for web accessibility, published by the W3C. They are organized around four principles, remembered by the acronym POUR:

Perceivable — information must be presentable to users in ways they can perceive. Content must not be invisible to all their senses.

Operable — interface components and navigation must be operable. Users must be able to operate the interface.

Understandable — information and the operation of the interface must be understandable. Content must not be beyond users’ understanding.

Robust — content must be robust enough to be interpreted by a wide variety of user agents, including assistive technologies.

WCAG has three conformance levels:

LevelWhat it meansWho requires it
AMinimum — without this, some users cannot access content at allEvery application, no exceptions
AAStandard — the level most regulations and legal requirements referenceMost commercial and government applications
AAAEnhanced — not required in full, but guidelines to aspire toSpecialized applications serving users who need the highest level of support

When a client, employer, or government procurement requirement says “WCAG 2.1 AA compliant,” they mean the application meets all Level A and Level AA success criteria in WCAG version 2.1.

Important: WCAG compliance is not the same as accessibility. It is possible to build an application that passes every automated WCAG check and is still unusable by a screen reader user because the logical flow of the content makes no sense when read linearly. Standards provide a floor, not a ceiling. Real accessibility requires testing with real users and real assistive technologies.

The Accessibility Tree — The Mental Model That Changes How You Write HTML

This is the concept I wish I had understood earlier, because it reframes every HTML decision you make.

When a browser loads a page, it builds two trees: the DOM tree (which JavaScript manipulates) and the accessibility tree — a parallel representation of the page that assistive technologies consume. The accessibility tree contains only the information that is meaningful to a screen reader or other assistive technology: roles, names, states, and properties.

Not everything in the DOM makes it to the accessibility tree. Pure presentational elements do not. What does make it is determined by the semantics of the HTML elements you use.

<!-- These two look identical visually but are completely different
     from an accessibility perspective -->

<!-- ❌ A div styled as a button — nothing in the accessibility tree
     signals that this is interactive or what it does -->
<div class="btn btn--primary" onclick="submitForm()">Submit</div>
<!-- Accessibility tree: generic container, text "Submit"
     No role. No keyboard operability. No state signals. -->

<!-- ✅ A real button — rich accessibility information automatically -->
<button type="submit" class="btn btn--primary">Submit</button>
<!-- Accessibility tree: role=button, name="Submit", focusable,
     activatable by Enter/Space, announces state changes automatically -->

The <button> element gives you keyboard focusability, Enter/Space activation, correct role signalling, and focus visible state — for free, because the browser knows what a button is. The <div> gives you none of these. You would have to add role="button", tabindex="0", and keyboard event handlers manually — and you would still be approximating what the native element provides.

This is the foundational principle: use the right semantic element and the accessibility comes with it. The accessibility tree is your application as assistive technology sees it, and it is built from your HTML semantics.

The Prerequisites: What You Need to Understand Before Building Accessibly

Semantic HTML — genuinely, not superficially

Every accessibility improvement you can make starts with using the right HTML element. Most developers know this as a principle but do not apply it consistently because they have never internalized what each element signals.

The elements that matter most:

<!-- Headings — the structure of the page for screen reader navigation
     Users can jump between headings. The hierarchy must be meaningful. -->
<h1>Account Management</h1>       <!-- one per page, the main topic -->
<h2>Open Tickets</h2>             <!-- major sections -->
<h3>Ticket #1042</h3>             <!-- subsections -->
<!-- Never skip levels (h1 → h3). Never use headings for visual size. -->

<!-- Landmarks — regions that screen reader users can jump between -->
<header>   <!-- banner landmark -->
<nav>      <!-- navigation landmark — list the available links here -->
<main>     <!-- main content — one per page -->
<aside>    <!-- complementary landmark -->
<footer>   <!-- contentinfo landmark -->
<section aria-labelledby="section-heading"> <!-- named region -->

<!-- Lists — signal groupings to screen readers -->
<ul>  <!-- unordered — group of equivalent items -->
<ol>  <!-- ordered — sequence matters -->
<!-- Screen readers announce "list, 5 items" before reading the list
     Users can skip the list or navigate item by item -->

<!-- Tables — for tabular data only, never for layout -->
<table>
  <caption>Open Tickets by Priority</caption>  <!-- table title -->
  <thead>
    <tr>
      <th scope="col">Ticket ID</th>   <!-- scope tells screen readers
      <th scope="col">Subject</th>       what the header applies to -->
      <th scope="col">Priority</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>1042</td>
      <td>Login broken</td>
      <td>Critical</td>
    </tr>
  </tbody>
</table>

<!-- Forms — the most accessibility-sensitive UI pattern -->
<form>
  <label for="email">Email address</label>  <!-- always label every input -->
  <input
    type="email"
    id="email"
    name="email"
    autocomplete="email"        <!-- helps users with memory impairments -->
    aria-describedby="email-error"
    aria-invalid="true"         <!-- when validation fails -->
  >
  <span id="email-error" role="alert">Please enter a valid email address</span>
</form>

Focus management — the behavior that makes keyboard navigation work

Focus is the position of keyboard interaction in the page. When you press Tab, focus moves to the next interactive element. When you press Enter or Space, you activate the focused element.

Accessible applications manage focus deliberately:

// When a modal opens — move focus inside it
// When it closes — return focus to the trigger

@Component({
  /* ... */
})
export class ModalComponent implements OnInit {
  @ViewChild('firstFocusableElement') firstElement!: ElementRef;
  @Input() triggerElement!: HTMLElement;

  ngOnInit() {
    // Move focus into the modal when it opens
    // Without this, keyboard users are stranded outside
    setTimeout(() => {
      this.firstElement.nativeElement.focus();
    });
  }

  close() {
    this.closed.emit();
    // Return focus to the element that opened the modal
    // Without this, keyboard users lose their place in the page
    this.triggerElement.focus();
  }
}

Focus traps in modals — preventing Tab from cycling outside the modal while it is open — are required for keyboard users:

// Focus trap — keep focus inside the modal
@Directive({ selector: '[appFocusTrap]' })
export class FocusTrapDirective {
  @HostListener('keydown', ['$event'])
  onKeydown(event: KeyboardEvent) {
    if (event.key !== 'Tab') return;

    const focusable = this.el.nativeElement.querySelectorAll(
      'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
    );
    const first = focusable[0];
    const last = focusable[focusable.length - 1];

    if (event.shiftKey && document.activeElement === first) {
      last.focus();
      event.preventDefault();
    } else if (!event.shiftKey && document.activeElement === last) {
      first.focus();
      event.preventDefault();
    }
  }
}

ARIA — what it is and when to use it

ARIA (Accessible Rich Internet Applications) is a set of HTML attributes that supplement native semantics when native HTML is not sufficient. The first rule of ARIA is the most important thing to know about it:

Do not use ARIA if you can use native HTML instead.

ARIA does not add behaviour. It adds information to the accessibility tree. When you add role="button" to a div, you have told assistive technology “this is a button” — but you have not made it keyboard-focusable, you have not made it respond to Enter/Space, you have not given it the visual focus indicator a button gets. You have described a button without building one.

When ARIA is appropriate:

<!-- Pattern 1: Dynamic content announcements — live regions -->
<!-- Screen reader users cannot see DOM changes visually.
     aria-live announces them without requiring focus to move. -->
<div
  aria-live="polite"         <!-- announce when the user is idle -->
  aria-atomic="true"         <!-- read the whole region, not just the changed part -->
  id="status-message"
  class="sr-only">           <!-- visually hidden, but in accessibility tree -->
  <!-- Angular/React updates this when async operations complete -->
</div>

<!-- Pattern 2: Labelling icon-only buttons -->
<button aria-label="Close dialog">

  <svg aria-hidden="true">  <!-- hide the icon from screen readers —
    <path d="..."/>           the label on the button is sufficient -->
  </svg>
</button>

<!-- Pattern 3: Expanded/collapsed state -->
<button
  aria-expanded="false"     <!-- updated dynamically when panel opens -->
  aria-controls="faq-panel-1">
  What is a ticket?
</button>
<div id="faq-panel-1" hidden>
  A ticket is a tracked unit of work...
</div>

<!-- Pattern 4: Progress and status -->
<div
  role="progressbar"
  aria-valuenow="65"
  aria-valuemin="0"
  aria-valuemax="100"
  aria-label="File upload progress">
</div>

<!-- Pattern 5: Error associations -->
<input
  id="ticket-subject"
  aria-describedby="subject-error subject-hint"
  aria-invalid="true">
<span id="subject-hint">Maximum 200 characters</span>
<span id="subject-error" role="alert">
  Subject is required
</span>

How to Build Accessibility In from the Start

The component audit before you build

Before writing a component, ask: what does this need to be accessible?

The questions are the same for every component:

  1. Can a keyboard user reach every interactive element by pressing Tab?
  2. Can they activate every interactive element with Enter or Space?
  3. Does the component communicate its state (expanded, selected, invalid) to assistive technology?
  4. Does it manage focus when its structure changes (modal opens, content loads, error appears)?
  5. Does all interactive content have a meaningful text label?
  6. Does color-coded information also communicate through text, shape, or pattern?

These are not hard questions. The challenge is building the habit of asking them before writing the component rather than after.

In Angular — what the framework gives you

Angular provides tooling that makes accessible components easier to build correctly. The CDK (Component Development Kit) includes utilities that should be in every serious Angular project:

// @angular/cdk/a11y — the accessibility utilities every Angular project needs
import { A11yModule, FocusTrap, LiveAnnouncer } from '@angular/cdk/a11y';

// LiveAnnouncer — announce messages to screen readers without moving focus
@Component({
  /* ... */
})
export class TicketOperationsComponent {
  constructor(private announcer: LiveAnnouncer) {}

  async resolveTicket(id: string) {
    await this.ticketFacade.resolve(id);
    // After the operation completes, announce the result
    // The user does not need to navigate to find out what happened
    this.announcer.announce('Ticket resolved successfully', 'polite');
  }

  async deleteTicket(id: string) {
    await this.ticketFacade.delete(id);
    this.announcer.announce('Ticket deleted', 'assertive');
    // 'assertive' interrupts the screen reader immediately
    // Use only for critical changes — success/failure of an action the user just took
  }
}
// FocusTrap — confine focus to a region (modal, drawer, popover)
@Component({
  selector: 'app-modal',
  template: `<div cdkTrapFocus cdkTrapFocusAutoCapture>...</div>`,
  // cdkTrapFocusAutoCapture moves focus inside automatically
})
export class ModalComponent {}
// Accessibility testing in unit tests — using @angular/cdk/testing
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { MatButtonHarness } from '@angular/material/button/testing';

it('should announce ticket resolution to screen readers', async () => {
  const loader = TestbedHarnessEnvironment.loader(fixture);
  const announcer = TestBed.inject(LiveAnnouncer);
  const spy = spyOn(announcer, 'announce');

  const resolveButton = await loader.getHarness(
    MatButtonHarness.with({ text: 'Resolve' })
  );
  await resolveButton.click();

  expect(spy).toHaveBeenCalledWith('Ticket resolved successfully', 'polite');
});

The color contrast requirement — the one you will get wrong if you do not check

WCAG AA requires a contrast ratio of at least 4.5:1 for normal text and 3:1 for large text. Most design systems handle this, but custom color combinations frequently fail without checking.

// Build contrast checking into your design token system
// Rather than checking after the fact, make the violation visible at design time

// ❌ These combinations frequently fail
const dangerousCombo = {
  background: '#F5C842', // yellow
  text: '#FFFFFF', // white — contrast ratio: 1.6:1 — fails
};

const anotherDangerous = {
  background: '#4CAF50', // green
  text: '#FFFFFF', // white — contrast ratio: 2.8:1 — fails for normal text
};

// Tools to check: WebAIM Contrast Checker, browser DevTools color picker,
// axe DevTools, Storybook a11y addon
/* CSS custom properties that enforce the design token approach.
   Every text/background combination derives from the token layer
   where contrast has been verified at design time */
:root {
  --color-text-on-primary: #1a1a1a; /* verified: 8.2:1 on --color-primary */
  --color-text-on-surface: #1a1a1a; /* verified: 16:1 on --color-surface */
  --color-text-secondary: #595959; /* verified: 7:1 on white */
  --color-text-disabled: #767676; /* verified: 4.54:1 — minimum passing */
}

A skip link allows keyboard users to jump past repetitive navigation to the main content. It is the first interactive element on every page, and it is usually visually hidden until focused.

<!-- The first element in the <body> -->
<a href="#main-content" class="skip-link"> Skip to main content </a>

<!-- ... navigation, header ... -->

<main id="main-content" tabindex="-1">
  <!-- tabindex="-1" makes it programmatically focusable
       so the browser moves focus here when the skip link is followed -->
</main>
/* Visually hidden but visible when focused */
.skip-link {
  position: absolute;
  transform: translateY(-100%);
  transition: transform 0.2s;
  background: var(--color-primary);
  color: var(--color-text-on-primary);
  padding: 0.5rem 1rem;
  border-radius: 0 0 0.25rem 0.25rem;
  text-decoration: none;
  font-weight: 500;
  z-index: 9999;
}

.skip-link:focus {
  transform: translateY(0);
  /* Now visible — keyboard user sees where they can skip to */
}

Testing — The Part That Cannot Be Automated Away

Automated accessibility testing is valuable and necessary. It is not sufficient. The frequently cited statistic is that automated tools catch 30–40% of real accessibility issues. The remaining 60–70% require manual testing.

Automated testing — the foundation

// With jest-axe — runs axe-core in your unit tests
import { axe, toHaveNoViolations } from 'jest-axe';

expect.extend(toHaveNoViolations);

it('ticket list should have no accessibility violations', async () => {
  const { container } = render(TicketListComponent, {
    providers: [
      /* ... */
    ],
    inputs: { tickets: mockTickets },
  });

  const results = await axe(container);
  expect(results).toHaveNoViolations();
});
// With Cypress and cypress-axe — accessibility tests in E2E
describe('Ticket management accessibility', () => {
  beforeEach(() => {
    cy.injectAxe();
  });

  it('should pass accessibility checks on the ticket list page', () => {
    cy.visit('/tickets');
    cy.checkA11y(null, {
      includedImpacts: ['critical', 'serious'],
      // Don't fail on 'moderate' violations in CI — address them separately
    });
  });

  it('should pass accessibility checks when the modal is open', () => {
    cy.visit('/tickets');
    cy.findByRole('button', { name: 'Create ticket' }).click();
    cy.checkA11y('[role="dialog"]');
  });
});

Manual testing — the only way to know it actually works

The minimum manual testing process I recommend for any accessible component:

Keyboard only — unplug your mouse:

  1. Tab through the component. Does every interactive element receive focus?
  2. Is the focus indicator visible at every step?
  3. Can you activate every button, link, and form control with Enter/Space?
  4. Does Tab order match the visual reading order?
  5. When a modal opens, does focus move inside it?
  6. When a modal closes, does focus return to the trigger?

With a screen reader:

Turn on VoiceOver (Mac: Cmd+F5) or NVDA (Windows: free download) and navigate the component. The test is not whether it reads the words — it is whether the experience makes sense as an audio-only interaction.

## What to listen for with a screen reader

✅ "Navigation, list, 4 items" — screen reader announces the landmark and count
✅ "Ticket 1042, Login broken, Critical priority, button" — full context before interaction
✅ "Ticket resolved successfully" — operation result announced without navigation

❌ "Button" — no label, user doesn't know what it does
❌ "Image" — icon with no alt text
❌ "Link" — multiple "Read more" links with no destination context
❌ (silence) — content that is invisible to the accessibility tree

Zoom to 200%:

WCAG requires that content remains usable at 200% zoom. Open your browser zoom to 200% and check that text does not overflow containers, horizontal scrollbars do not appear on the main content area, and nothing is clipped or truncated.

color contrast check:

Browser DevTools color picker shows contrast ratios. Check every text/background combination in your component, including hover and focus states.

How Accessibility Affects the People Using Your Application

This is the section most accessibility guides skip, because the technical details are more comfortably quantified. But the human dimension is the reason the technical details matter.

The person who cannot see the status indicator

On the NCR Vision terminal monitoring platform, every terminal had a status indicator — a colored dot, green for online, red for offline. The first version communicated status entirely through color. For a person with color blindness, every terminal looked the same.

The fix was straightforward: add a text label and a shape indicator alongside the color. Green circle + “Online” text. Red square + “Offline” text. Three types of information instead of one. It took less than an hour.

But before the fix, a user with deuteranopia — the most common form of red-green color blindness, affecting approximately 1 in 12 men — could not use the dashboard for its primary purpose. Not because the feature was complex. Because the default design assumed a specific kind of vision.

That assumption is invisible until you stop assuming it.

The person who navigates by keyboard

On the 1mg healthcare dashboard, a user managing their medications through keyboard navigation alone — perhaps because of a motor impairment, perhaps because of a broken mouse, perhaps because keyboard is simply faster for them — would encounter a form where Tab skipped several fields entirely. Interactive elements that were not reachable by keyboard. A date picker that required mouse interaction to open.

These are not minor inconveniences for a person who does not use a mouse. They are blockers. The application simply does not work for them.

The WCAG principle Operable exists because an interface that cannot be operated is not an interface at all — it is a visual display that excludes everyone who cannot operate it in the specific way it was designed for.

The person who relies on screen readers in healthcare

The 1mg project was a healthcare dashboard. People using it were managing their medications, their prescriptions, their health records. For a user who is blind or has severe low vision, the accuracy and completeness of the information read by their screen reader is not a usability concern — it is a safety concern.

An icon button with no label. “Button.” Press Enter. What happened? Did something get deleted? Was a medication dose confirmed? The visual context that sighted users take for granted is completely absent.

In healthcare, in finance, in government services — in any application where decisions have real consequences — the absence of accessibility is not just an inconvenience. It is a material harm.

The Developer Experience Dimension

Accessibility is often discussed as a cost — additional work, additional testing, additional constraints on design decisions. That framing is real but incomplete. The developer experience of working on an accessible codebase is meaningfully better than the developer experience of working on an inaccessible one, for several specific reasons.

Accessible code is testable code

Presentational components that communicate their state through ARIA attributes are easier to write tests for than components that communicate state visually. Testing libraries like Testing Library explicitly encourage accessibility-first test selectors:

// ❌ Fragile — depends on DOM structure and CSS classes
const button = container.querySelector('.btn.btn--primary.btn--icon');

// ✅ Robust — queries by role and name, as a user would experience it
const button = screen.getByRole('button', { name: 'Resolve ticket' });

// ❌ Fragile — depends on implementation details
const errorMsg = container.querySelector('[data-testid="error-msg"]');

// ✅ Robust — queries by role, accessible to screen readers by definition
const errorMsg = screen.getByRole('alert');

// ❌ Fragile — depends on visual structure
const statusIndicator = container.querySelector('.status-dot--online');

// ✅ Robust — tests what the user actually perceives
expect(screen.getByText('Online')).toBeInTheDocument();

This is not a coincidence. Accessible code exposes the right information to the accessibility tree. Testing Library reads from the accessibility tree. Tests written against the accessibility tree are tests written against the user experience — they verify what the user perceives, not what the developer implemented.

Semantic HTML is self-documenting

A codebase full of <div> and <span> elements requires CSS classes or comments to communicate intent. A codebase that uses semantic HTML — <nav>, <main>, <article>, <button>, <form> — communicates structure directly. The intent is in the element.

<!-- ❌ Without semantics — requires additional context to understand structure -->
<div class="sidebar-nav">
  <div class="nav-section">
    <div class="nav-section-header">Ticket Management</div>
    <div class="nav-item active">Open Tickets</div>
    <div class="nav-item">Resolved Tickets</div>
  </div>
</div>

<!-- ✅ With semantics — structure is self-evident -->
<nav aria-label="Ticket management">
  <section>
    <h2>Ticket Management</h2>
    <ul>
      <li><a href="/tickets/open" aria-current="page">Open Tickets</a></li>
      <li><a href="/tickets/resolved">Resolved Tickets</a></li>
    </ul>
  </section>
</nav>

Every developer who reads the second version knows immediately what it is and what each part does. No CSS class vocabulary required.

Keyboard navigation surfaces interaction bugs

Testing with a keyboard only is one of the most effective ways to find interaction bugs that visual testing misses. Focus management issues reveal themselves immediately. Race conditions in state transitions become visible because keyboard navigation is slower and more deliberate than mouse interaction. Components that open but do not close, menus that lose their state, forms that submit before validation — all of these appear during keyboard testing that might not appear during casual mouse-based testing.

Making keyboard testing a habit as part of development — not just accessibility testing, but interaction testing — improves the overall quality of the UI beyond the accessibility benefit.

The Accessibility Audit as Architecture Review

When I did the 1mg accessibility audit, the most important thing I learned was that the accessibility problems were not scattered randomly through the codebase. They clustered around specific architectural patterns:

  • Every <div> used as an interactive element was both an accessibility violation and a component where the semantic meaning had been traded away
  • Every component with inline event handlers that did not include keyboard event handlers was a place where interaction had been designed mouse-first and not revisited
  • Every form field without a label was a place where the visual proximity of the label had been trusted instead of the explicit association

These patterns were systematic. Fixing them at the component level required identifying the system-level habit that produced them — the default of reaching for <div> instead of semantic elements, the default of not checking keyboard operability during development, the default of relying on visual layout instead of explicit associations.

The accessibility audit was, in effect, a review of the team’s defaults. Changing the accessibility of the application required changing the defaults, not just fixing the violations. That is an architectural change — a change to how every component is built, not just to which components are fixed.

Conclusion

Accessibility is not a feature that you add. It is a quality of the work — like performance, like maintainability, like testability. It is built in by default or it is absent by default, and reversing the default after the fact is expensive and incomplete.

The developers who build accessibly as a matter of course are not spending more time on accessibility than the developers who do not. They are spending that time distributed differently — in the decisions made at component creation rather than in the audits and retrofits that come later. The total time is similar. The result is dramatically different.

The users who benefit from accessible applications are not a niche. They are the billion people worldwide with disabilities, plus the much larger number of people with situational and temporary impairments, plus every person who uses a keyboard because it is faster, plus every person in a context where their dominant interaction mode is unavailable. That is not a special audience. That is the full range of human variability that your application will encounter over its lifetime.

Build for that range from the start. Not because a compliance requirement demands it. Not because an audit found violations. Because the work is not complete when it excludes the people who will try to use it.