DEV Community

Rizwan Saleem
Rizwan Saleem

Posted on

How to build accessible web applications — a practical frontend tutorial

How to build accessible web applications — a practical frontend tutorial

Accessibility is not a finishing touch; it is a core quality of the web. When you build with accessibility in mind from the start, you create interfaces that are usable, resilient, and often simpler.

WCAG in Practice

The Web Content Accessibility Guidelines (WCAG) organize requirements under four principles:

  • Perceivable: Content can be seen or heard (e.g., text alternatives for images, captions for video).
  • Operable: Interfaces work with different inputs (e.g., keyboard navigation).
  • Understandable: Content and behavior are clear and predictable.
  • Robust: Works across browsers and assistive technologies.

Common targets:

  • Level AA is the standard most teams aim for.
  • Color contrast: at least $$4.5:1$$ for normal text, $$3:1$$ for large text.
  • Provide text alternatives and clear focus states.

Semantic HTML First

Use native elements before adding ARIA. Browsers and assistive tech already understand them.

  • Headings: <h1> to <h6> in logical order.
  • Landmarks: <header>, <nav>, <main>, <footer>.
  • Controls: <button> for actions, <a> for navigation, <label> with <input>.

Example:

<main>
  <h1>Account settings</h1>
  <form>
    <label for="email">Email address</label>
    <input id="email" name="email" type="email" required>
    <button type="submit">Save</button>
  </form>
</main>
Enter fullscreen mode Exit fullscreen mode

This already provides labels, roles, and keyboard support without extra code.

ARIA: Enhance, Don’t Replace

ARIA (Accessible Rich Internet Applications) fills gaps when native semantics are insufficient.

  • Use roles sparingly: role="dialog", role="tablist".
  • Use properties to describe state: aria-expanded="true", aria-selected="false".
  • Use relationships: aria-labelledby, aria-describedby.

Rules of thumb:

  • Do not use ARIA to fix incorrect HTML; fix the HTML.
  • If you can use a native element, prefer it over a custom role.

Example (disclosure):

<button aria-expanded="false" aria-controls="faq1" id="faqBtn1">
  What is your return policy?
</button>
<div id="faq1" role="region" aria-labelledby="faqBtn1" hidden>
  Returns are accepted within 30 days.
</div>
Enter fullscreen mode Exit fullscreen mode

Keyboard Navigation

Every interactive element must be usable with a keyboard.

  • Ensure logical tab order (DOM order matters).
  • Use Tab to move forward, Shift+Tab backward.
  • Provide visible focus styles; do not remove outlines without replacement.
  • Support common keys: Enter/Space to activate buttons, arrow keys for menus and tabs.

Checklist:

  • No keyboard traps.
  • Modals trap focus while open and return focus on close.
  • Skip links (e.g., “Skip to content”) at the top of the page.

Screen Reader Support

Screen readers rely on structure and labels.

  • Provide meaningful link text (avoid “click here”).
  • Use alt text for images; keep it concise and purposeful.
  • Announce dynamic updates with live regions when needed: aria-live="polite".

Example (status message):

<div aria-live="polite" id="status"></div>
Enter fullscreen mode Exit fullscreen mode

Update this element when actions complete so users are informed.

Colour and Contrast

Design for sufficient contrast and do not rely on color alone.

  • Meet $$4.5:1$$ contrast for body text.
  • Pair color with text or icons for states (e.g., error + message).
  • Check focus indicators and disabled states for visibility.

Accessible Forms and Validation

Forms are a common failure point; keep them explicit and forgiving.

  • Associate labels with inputs using for and id.
  • Group related fields with <fieldset> and <legend>.
  • Provide inline help and error messages.
  • Announce errors and move focus to the first error on submit.

Example:

<form novalidate>
  <fieldset>
    <legend>Contact</legend>

    <label for="name">Full name</label>
    <input id="name" name="name" required aria-describedby="nameHelp">
    <div id="nameHelp">Enter your first and last name.</div>

    <label for="email">Email</label>
    <input id="email" name="email" type="email" required>

    <div id="errors" aria-live="assertive"></div>
    <button type="submit">Submit</button>
  </fieldset>
</form>
Enter fullscreen mode Exit fullscreen mode

On validation failure, inject clear messages into #errors, link errors to fields with aria-describedby, and focus the first invalid input.

Testing: Automated and Manual

Automated tools catch common issues quickly; manual testing finds real usability problems.

Automated:

  • axe DevTools, Lighthouse, pa11y.
  • Integrate into CI to prevent regressions.

Manual:

  • Keyboard-only walkthrough of all flows.
  • Screen reader checks (NVDA or JAWS on Windows, VoiceOver on macOS/iOS).
  • Zoom to 200% and reflow; check responsive behavior.
  • Color contrast checks and high-contrast mode.

A useful routine:

  • Start with a keyboard pass.
  • Run automated scans and fix violations.
  • Do a screen reader pass on key pages.
  • Re-test after changes.

Building an Accessibility Mindset

Accessibility scales when it is embedded in how teams work.

  • Design: include contrast, focus states, and error patterns in the design system.
  • Engineering: build accessible components (buttons, modals, forms) once and reuse them.
  • Content: write clear labels, headings, and instructions.
  • QA: include accessibility acceptance criteria and test cases.
  • Governance: add linting rules and CI checks.

Define “done” to include accessibility, not as a separate task.

Real Example: Accessible Modal

Key requirements: focus management, keyboard support, semantics.

<button id="openModal">Open details</button>

<div id="modal" role="dialog" aria-modal="true" aria-labelledby="modalTitle" hidden>
  <h2 id="modalTitle">Order details</h2>
  <p>Order #12345</p>
  <button id="closeModal">Close</button>
</div>

<script>
const openBtn = document.getElementById('openModal');
const modal = document.getElementById('modal');
const closeBtn = document.getElementById('closeModal');
let lastFocused;

openBtn.addEventListener('click', () => {
  lastFocused = document.activeElement;
  modal.hidden = false;
  closeBtn.focus();
});

closeBtn.addEventListener('click', () => {
  modal.hidden = true;
  lastFocused.focus();
});

document.addEventListener('keydown', (e) => {
  if (e.key === 'Escape' && !modal.hidden) {
    modal.hidden = true;
    lastFocused.focus();
  }
});
</script>
Enter fullscreen mode Exit fullscreen mode

Enhancements to consider:

  • Trap focus within the modal while open.
  • Prevent background scrolling.
  • Restore focus reliably. Would you like this tailored into a checklist for your team or adapted to a specific framework like React or Vue?

Rizwan Saleem — https://rizwansaleem.co

Top comments (0)