ESC
Type to search guides, tutorials, and reference documentation.
Verified by Garnet Grid

Accessibility Engineering

Build accessible web applications that work for everyone. Covers WCAG compliance, semantic HTML, ARIA patterns, keyboard navigation, screen reader testing, and making accessibility a first-class engineering concern.

Accessibility is not a feature — it is a quality attribute of every feature. 15% of the world’s population lives with some form of disability. Beyond ethics and inclusion, accessibility failures create legal liability (ADA lawsuits increased 300% since 2018) and exclude paying customers.


WCAG Compliance Levels

LevelRequirementExamples
A (minimum)Content is accessibleText alternatives, keyboard navigation
AA (standard target)Usable by most peopleColor contrast 4.5:1, focus indicators
AAA (enhanced)Optimized for accessibilityColor contrast 7:1, extended audio descriptions

Target WCAG 2.1 Level AA — this is the legal standard in most jurisdictions and the right balance of effort vs. coverage.


Semantic HTML

The most impactful accessibility improvement is using the correct HTML elements:

<!-- BAD: Divs with click handlers -->
<div class="button" onclick="submitForm()">Submit</div>
<div class="nav">
  <div class="nav-item" onclick="goHome()">Home</div>
</div>

<!-- GOOD: Semantic elements -->
<button type="submit">Submit</button>
<nav aria-label="Main navigation">
  <a href="/">Home</a>
</nav>

Semantic HTML gives you keyboard navigation, screen reader support, and focus management for free.

Heading Hierarchy

<!-- CORRECT hierarchy -->
<h1>Product Catalog</h1>
  <h2>Electronics</h2>
    <h3>Laptops</h3>
    <h3>Phones</h3>
  <h2>Clothing</h2>
    <h3>Men's</h3>
    <h3>Women's</h3>

<!-- WRONG: Skipping levels, multiple h1s -->
<h1>Product Catalog</h1>
<h3>Electronics</h3>  <!-- Skipped h2 -->
<h1>Categories</h1>   <!-- Multiple h1s -->

ARIA Patterns

Use ARIA only when native HTML cannot express the UI pattern:

Live Regions

<!-- Announce dynamic content changes to screen readers -->
<div role="status" aria-live="polite">
  3 results found
</div>

<div role="alert" aria-live="assertive">
  Error: Payment failed. Please try again.
</div>

Tab Panel

<div role="tablist" aria-label="Account Settings">
  <button role="tab" aria-selected="true" aria-controls="panel-profile"
          id="tab-profile">Profile</button>
  <button role="tab" aria-selected="false" aria-controls="panel-security"
          id="tab-security" tabindex="-1">Security</button>
</div>

<div role="tabpanel" id="panel-profile" aria-labelledby="tab-profile">
  Profile content...
</div>
<div role="tabpanel" id="panel-security" aria-labelledby="tab-security" 
     hidden>
  Security content...
</div>

Keyboard Navigation

Every interactive element must be reachable and operable via keyboard:

// Focus trap for modals
function trapFocus(modal) {
  const focusableElements = modal.querySelectorAll(
    'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
  );
  const first = focusableElements[0];
  const last = focusableElements[focusableElements.length - 1];
  
  modal.addEventListener('keydown', (e) => {
    if (e.key === 'Tab') {
      if (e.shiftKey && document.activeElement === first) {
        e.preventDefault();
        last.focus();
      } else if (!e.shiftKey && document.activeElement === last) {
        e.preventDefault();
        first.focus();
      }
    }
    if (e.key === 'Escape') closeModal();
  });
  
  first.focus();
}

Focus Indicators

/* Never remove focus outlines without replacement */
/* BAD */
*:focus { outline: none; }

/* GOOD: Custom focus indicator */
:focus-visible {
  outline: 3px solid var(--color-focus);
  outline-offset: 2px;
  border-radius: 2px;
}

Color and Contrast

/* WCAG AA: 4.5:1 contrast ratio for normal text */
/* WCAG AA: 3:1 contrast ratio for large text (18px+ or 14px+ bold) */

/* Check: https://webaim.org/resources/contrastchecker/ */

/* Never use color alone to convey information */
/* BAD */
.error { color: red; }

/* GOOD: Color + icon + text */
.error { 
  color: var(--color-error);
  border-left: 4px solid var(--color-error);
}
.error::before { content: "⚠ "; }

Testing

Automated

# axe-core in CI
npx @axe-core/cli https://localhost:3000

# Lighthouse accessibility audit
npx lighthouse https://localhost:3000 --only-categories=accessibility

# jest-axe for component tests
import { axe, toHaveNoViolations } from 'jest-axe';
expect.extend(toHaveNoViolations);

test('button has no accessibility violations', async () => {
  const { container } = render(<Button>Click me</Button>);
  const results = await axe(container);
  expect(results).toHaveNoViolations();
});

Manual

Screen readers: VoiceOver (Mac), NVDA (Windows), TalkBack (Android)
Keyboard only:  Navigate entire app without mouse
Zoom:           200% zoom, no horizontal scrolling
Color:          Grayscale mode, high contrast mode
Motion:         prefers-reduced-motion respected

Anti-Patterns

Anti-PatternConsequenceFix
div and span for everythingNo keyboard/screen reader supportUse semantic HTML elements
Focus outlines removedKeyboard users cannot see focusCustom :focus-visible styles
Images without alt textScreen readers say “image” with no contextDescriptive alt text (or alt="" for decorative)
Color-only indicatorsColorblind users miss informationUse color + icon + text
Accessibility as an afterthoughtRetrofitting is 10x more expensiveBuild accessible from the start

Accessibility is a technical requirement, not a checkbox. Build it into your component library, your review process, and your testing pipeline.

Jakub Dimitri Rezayev
Jakub Dimitri Rezayev
Founder & Chief Architect • Garnet Grid Consulting

Jakub holds an M.S. in Customer Intelligence & Analytics and a B.S. in Finance & Computer Science from Pace University. With deep expertise spanning D365 F&O, Azure, Power BI, and AI/ML systems, he architects enterprise solutions that bridge legacy systems and modern technology — and has led multi-million dollar ERP implementations for Fortune 500 supply chains.

View Full Profile →