DEV Community

Weather Clock Dash
Weather Clock Dash

Posted on

Web Accessibility: Semantic HTML, ARIA, Keyboard Navigation, and Testing

Web accessibility (a11y) is about building interfaces that work for everyone — including people who use screen readers, keyboard navigation, switch controls, or have cognitive disabilities. It's also increasingly a legal requirement in many jurisdictions.

The good news: accessible HTML is mostly just correct HTML. Most accessibility mistakes come from using the wrong element for a job, or overriding browser defaults that were already correct.

Semantic HTML First

The single most impactful accessibility improvement is using semantic HTML:

<!-- Wrong: everything is a div -->
<div class="button" onclick="submit()">Submit</div>
<div class="header"><div class="nav">...</div></div>

<!-- Right: semantic elements -->
<button type="button" onclick="submit()">Submit</button>
<header><nav>...</nav></header>
Enter fullscreen mode Exit fullscreen mode

Semantic elements come with built-in accessibility: <button> is keyboard-focusable, activatable with Enter/Space, and announced as "button" by screen readers. A <div> is none of these things without significant ARIA work.

Key Semantic Elements

<main>        <!-- primary page content, one per page -->
<header>      <!-- page or section header -->
<nav>         <!-- navigation links -->
<aside>       <!-- secondary content (sidebar, related links) -->
<footer>      <!-- page or section footer -->
<article>     <!-- self-contained content (blog post, comment) -->
<section>     <!-- thematic grouping with a heading -->
<h1> - <h6>   <!-- headings: use in order, don't skip levels -->
<ul>, <ol>    <!-- lists: screen readers announce item count -->
<table>       <!-- tabular data: NOT for layout -->
<figure>      <!-- image with caption -->
<figcaption>  <!-- caption for figure -->
Enter fullscreen mode Exit fullscreen mode

Images

Every image needs an alt attribute:

<!-- Informative image: describe what the image shows -->
<img src="chart.png" alt="Bar chart showing 40% increase in Firefox users Q3 2024">

<!-- Decorative image: empty alt, screen readers skip it -->
<img src="decorative-line.svg" alt="">

<!-- Icon button: describe the action, not the icon -->
<button>
  <img src="delete.svg" alt="Delete item">
</button>

<!-- Complex image: use a figcaption for detail -->
<figure>
  <img src="architecture-diagram.png" alt="System architecture diagram showing three services">
  <figcaption>The data pipeline connects the ingestion service to the processing queue and storage layer.</figcaption>
</figure>
Enter fullscreen mode Exit fullscreen mode

SVG icons inline in buttons need a title or aria-label:

<button aria-label="Close dialog">
  <svg aria-hidden="true" focusable="false"><!-- icon --></svg>
</button>
Enter fullscreen mode Exit fullscreen mode

Forms

<form>
  <!-- Every input needs a label -->
  <label for="email">Email address</label>
  <input type="email" id="email" name="email" required
         autocomplete="email"
         aria-describedby="email-hint">
  <p id="email-hint">We'll never share your email.</p>

  <!-- Error messages -->
  <input type="text" id="username"
         aria-invalid="true"
         aria-describedby="username-error">
  <p id="username-error" role="alert">Username already taken</p>

  <!-- Group related inputs -->
  <fieldset>
    <legend>Notification preferences</legend>
    <label>
      <input type="checkbox" name="email-notifs"> Email notifications
    </label>
    <label>
      <input type="checkbox" name="sms-notifs"> SMS notifications
    </label>
  </fieldset>
</form>
Enter fullscreen mode Exit fullscreen mode

aria-describedby links the input to its hint/error message. Screen readers announce the description after the input label.

role="alert" announces content immediately when it's added to the DOM — good for validation errors.

Keyboard Navigation

All interactive elements must be keyboard-accessible:

// Keyboard event handler (don't rely only on click)
element.addEventListener('keydown', (e) => {
  if (e.key === 'Enter' || e.key === ' ') {
    e.preventDefault();
    activate();
  }
});
Enter fullscreen mode Exit fullscreen mode

Tab order: Users navigate with Tab (forward) and Shift+Tab (backward). The default order follows DOM order — don't use tabindex values > 0 (they mess up natural order):

<!-- tabindex=0: include in tab order at natural position -->
<div role="button" tabindex="0">Custom button</div>

<!-- tabindex=-1: focusable programmatically, not via Tab -->
<div id="modal" tabindex="-1">Modal content</div>

<!-- tabindex=1+: avoid — overrides natural tab order -->
<input tabindex="3"> <!-- don't do this -->
Enter fullscreen mode Exit fullscreen mode

Skip navigation: Let keyboard users skip to main content:

<a href="#main-content" class="skip-link">Skip to main content</a>
<main id="main-content">...</main>
Enter fullscreen mode Exit fullscreen mode
.skip-link {
  position: absolute;
  top: -100%;
}
.skip-link:focus {
  top: 0;  /* appears when focused */
}
Enter fullscreen mode Exit fullscreen mode

ARIA Roles and Labels

ARIA (Accessible Rich Internet Applications) supplements HTML semantics. Use it when native HTML can't express the role:

<!-- Custom toggle button -->
<button aria-pressed="false" id="dark-mode-toggle">Dark mode</button>

<script>
  const btn = document.getElementById('dark-mode-toggle');
  btn.addEventListener('click', () => {
    const pressed = btn.getAttribute('aria-pressed') === 'true';
    btn.setAttribute('aria-pressed', !pressed);
  });
</script>

<!-- Expanded/collapsed state -->
<button aria-expanded="false" aria-controls="menu">Menu</button>
<ul id="menu" hidden>...</ul>

<!-- Live regions: announce dynamic updates -->
<div aria-live="polite" id="status-message"></div>
<!-- Polite: announces after current speech ends -->
<!-- Assertive: interrupts immediately (use sparingly) -->
Enter fullscreen mode Exit fullscreen mode

ARIA first rule: If a native HTML element has the semantics you need, use it. ARIA is for custom widgets only.

Color and Contrast

WCAG 2.1 requires a contrast ratio of 4.5:1 for normal text, 3:1 for large text (18pt+ or 14pt+ bold).

Check with browser DevTools:

Never convey information through color alone:

<!-- Wrong: only color distinguishes the error -->
<span style="color: red">Required field</span>

<!-- Right: color + text/icon -->
<span style="color: red" aria-label="Required">
  ⚠ Required field
</span>
Enter fullscreen mode Exit fullscreen mode

Focus Styles

Never just remove focus outlines — keyboard users rely on them:

/* Don't do this */
*:focus { outline: none; }

/* Better: custom focus style */
:focus-visible {
  outline: 2px solid #2563eb;
  outline-offset: 2px;
}
Enter fullscreen mode Exit fullscreen mode

:focus-visible only shows the outline when using keyboard navigation, not on mouse clicks. This is the modern approach — no need to suppress focus rings for mouse users.

Modal Dialogs

Modals have specific accessibility requirements:

// When modal opens:
// 1. Move focus into the modal
// 2. Trap focus inside
// 3. Return focus to trigger when closed

function openModal(modal, trigger) {
  modal.removeAttribute('hidden');
  modal.querySelector('[autofocus], button, [href], input').focus();

  // Trap focus
  modal.addEventListener('keydown', trapFocus);

  // Close on Escape
  modal.addEventListener('keydown', (e) => {
    if (e.key === 'Escape') closeModal(modal, trigger);
  });
}

function closeModal(modal, trigger) {
  modal.setAttribute('hidden', '');
  trigger.focus();  // return focus
  modal.removeEventListener('keydown', trapFocus);
}
Enter fullscreen mode Exit fullscreen mode

Native <dialog> handles all of this automatically — use it:

<dialog id="my-modal">
  <h2>Dialog title</h2>
  <p>Content</p>
  <button autofocus>Close</button>
</dialog>
Enter fullscreen mode Exit fullscreen mode
document.getElementById('my-modal').showModal();
Enter fullscreen mode Exit fullscreen mode

Firefox has supported native <dialog> since Firefox 98 (2022).

Testing Accessibility

Manual testing:

  1. Navigate the page with Tab only — can you reach everything?
  2. Test with Firefox Reader View — does the content make sense?
  3. Use Firefox Accessibility Inspector (DevTools → Accessibility tab)

Screen reader testing:

  • macOS: VoiceOver (Cmd+F5)
  • Windows: NVDA (free) or JAWS
  • iOS: VoiceOver
  • Android: TalkBack

Automated audits:

  • Firefox DevTools: Accessibility panel → check for warnings
  • Lighthouse: npx lighthouse --categories=accessibility https://yoursite.com
  • axe browser extension

Automated tools catch about 30-40% of accessibility issues. Manual testing and real user testing are required for the rest.

The most important shift: think of accessibility not as a checklist to pass but as correct implementation. Semantic HTML, proper labels, and keyboard support aren't extras — they're how the web is supposed to work.

Top comments (0)