Web development

Web accessibility 2026: complete checklist for developers

David Morales VegaSeptember 15, 202515 min read

Originally published on Bemorex Learn, our learning platform. · Leer en español

Why web accessibility matters

Web accessibility (a11y) is not an extra or a "nice to have." It is a fundamental requirement that benefits all your users:

  • 1 in 5 people has some form of disability
  • Users with temporary disabilities (broken arm, eye infection)
  • Users in situational contexts (sun on the screen, hands occupied)
  • Older users with reduced vision or mobility
  • Improves SEO and overall usability

Additionally, in many countries web accessibility is a legal requirement. The European Accessibility Act takes effect in June 2025, and the ADA in the United States has already generated thousands of lawsuits over inaccessible websites.

WCAG 2.2: the four principles

The Web Content Accessibility Guidelines are organized around four fundamental principles, known as POUR:

1. Perceivable

Content must be perceivable through all available senses.

2. Operable

The interface must be operable by any user with any input device.

3. Understandable

The content and interface must be understandable.

4. Robust

Content must be interpretable by current and future assistive technologies.

Complete checklist by category

Structure and semantics

  • [ ] Use a single <main> per page
  • [ ] Headings in hierarchical order without gaps (h1 -> h2 -> h3)
  • [ ] Correct landmarks: <header>, <nav>, <main>, <aside>, <footer>
  • [ ] Semantic lists for groups of items (<ul>, <ol>, <dl>)
  • [ ] Tables with <caption>, <thead>, <th scope="col|row">
  • [ ] Document language declared: <html lang="es">
  • [ ] Language changes marked: <span lang="en">Responsive design</span>

Images and multimedia

  • [ ] All informative images have a descriptive alt
  • [ ] Decorative images use alt="" or aria-hidden="true"
  • [ ] Complex graphics have a long description with aria-describedby
  • [ ] Videos have subtitles
  • [ ] Audio has a transcript available
  • [ ] Animations can be paused or disabled
  • [ ] Respect prefers-reduced-motion:
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

Keyboard navigation

  • [ ] All interactive elements are reachable with Tab
  • [ ] Tab order is logical and predictable
  • [ ] Focus is visible at all times (:focus-visible)
  • [ ] There are no keyboard traps (user can exit with Tab/Escape)
  • [ ] A "Skip to content" link exists
  • [ ] Modals trap focus correctly (focus trapping)
  • [ ] Escape closes modals and popups
  • [ ] Keyboard shortcuts do not interfere with the browser

Forms

  • [ ] Each input has a <label> associated with for/id
  • [ ] Input groups use <fieldset> and <legend>
  • [ ] Required fields have aria-required="true" or required
  • [ ] Errors are announced with aria-describedby and aria-invalid
  • [ ] Autocomplete uses autocomplete with correct values
  • [ ] Format instructions are visible, not only as placeholder
<div class="form-group">
  <label for="email">Email address</label>
  <input
    id="email"
    type="email"
    required
    autocomplete="email"
    aria-describedby="email-help email-error"
  />
  <p id="email-help" class="hint">Example: [email protected]</p>
  <p id="email-error" class="error" role="alert" aria-live="assertive">
    <!-- Dynamic error message -->
  </p>
</div>

Color and contrast

  • [ ] Minimum contrast ratio 4.5:1 for normal text (AA)
  • [ ] Minimum contrast ratio 3:1 for large text (24px+ or 19px+ bold)
  • [ ] Minimum contrast ratio 3:1 for UI components and graphics
  • [ ] Information is not conveyed by color alone
  • [ ] Works correctly in high contrast mode

Verify contrast programmatically

// Calculate the contrast ratio between two colors
function contrastRatio(luminance1, luminance2) {
  const lighter = Math.max(luminance1, luminance2);
  const darker = Math.min(luminance1, luminance2);
  return (lighter + 0.05) / (darker + 0.05);
}

function relativeLuminance(r, g, b) {
  const [rs, gs, bs] = [r, g, b].map(c => {
    c = c / 255;
    return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
  });
  return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs;
}

Dynamic content and ARIA

  • [ ] Dynamic changes are announced with aria-live
  • [ ] Loading states use aria-busy="true"
  • [ ] Tabs use roles tablist, tab, tabpanel
  • [ ] Accordions use aria-expanded
  • [ ] Menus use roles menu, menuitem
  • [ ] Tooltips are linked with aria-describedby
  • [ ] Toggle buttons use aria-pressed
<!-- Accessible tabs -->
<div role="tablist" aria-label="Course sections">
  <button role="tab"
          id="tab-1"
          aria-selected="true"
          aria-controls="panel-1">
    Lesson 1
  </button>
  <button role="tab"
          id="tab-2"
          aria-selected="false"
          aria-controls="panel-2"
          tabindex="-1">
    Lesson 2
  </button>
</div>

<div role="tabpanel"
     id="panel-1"
     aria-labelledby="tab-1">
  Lesson 1 content...
</div>

<div role="tabpanel"
     id="panel-2"
     aria-labelledby="tab-2"
     hidden>
  Lesson 2 content...
</div>

Focus management in SPAs

In Single Page Applications (SPA), focus management is critical:

// Angular: move focus to the main content after navigating
import { Router, NavigationEnd } from '@angular/router';
import { inject } from '@angular/core';

export class AppComponent {
  private router = inject(Router);

  constructor() {
    this.router.events.subscribe(event => {
      if (event instanceof NavigationEnd) {
        const main = document.querySelector('main');
        if (main) {
          main.setAttribute('tabindex', '-1');
          main.focus();
        }
      }
    });
  }
}

Testing tools

Automated

Tool Type Coverage
axe DevTools Browser extension ~57% of WCAG issues
Lighthouse Chrome DevTools General audit
pa11y CLI / CI Automation
eslint-plugin-jsx-a11y Linter Prevention in code
jest-axe Unit testing Automated tests

Essential manual testing

Automated tools only detect 30-50% of accessibility issues. Manual testing is mandatory:

  1. Navigate with keyboard only: Tab, Shift+Tab, Enter, Space, Escape, arrows
  2. Use a screen reader: NVDA (Windows), VoiceOver (Mac/iOS), TalkBack (Android)
  3. Verify at 200% zoom: everything must be functional and readable
  4. Test without colors: use a color blindness simulation extension
  5. Verify in high contrast mode: Windows High Contrast Mode

The 10 most common mistakes

  1. Images without alt attribute
  2. Forms without associated labels
  3. Insufficient contrast
  4. No visible focus indicator
  5. Generic links like "click here" or "read more" without context
  6. Content only accessible with mouse (hover)
  7. Modals without focus trapping
  8. No skip link
  9. Videos without subtitles
  10. Incorrect use of ARIA (worse than not using ARIA)

Conclusion

Web accessibility is not a phase of development; it is a design philosophy. Every decision, from HTML structure to colors and interactions, impacts the experience of millions of people. This checklist is a solid starting point, but true accessibility comes from empathizing with your users and continuously testing with real people and assistive technologies.

Code examples

HTML

<!-- Region that announces changes to screen readers -->
<div aria-live="polite" aria-atomic="true" class="sr-only">
  {{ statusMessage }}
</div>

<!-- Button with accessible toggle state -->
<button
  type="button"
  [attr.aria-pressed]="isBookmarked()"
  [attr.aria-label]="isBookmarked()
    ? 'Remove from favorites'
    : 'Add to favorites'"
  (click)="toggleBookmark()">
  <svg aria-hidden="true"><!-- icon --></svg>
</button>

<!-- Skip link for keyboard navigation -->
<a href="#main-content" class="skip-link">
  Skip to main content
</a>

CSS

/* sr-only class: visible only to screen readers */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* Skip link: visible only on focus */
.skip-link {
  position: absolute;
  top: -100%;
  left: 50%;
  transform: translateX(-50%);
  z-index: 100;
  padding: 0.75rem 1.5rem;
  background: var(--color-primary);
  color: var(--color-on-primary);
  border-radius: 0 0 0.5rem 0.5rem;
  text-decoration: none;
  font-weight: 600;
}

.skip-link:focus {
  top: 0;
}

/* Visible focus indicator */
:focus-visible {
  outline: 3px solid var(--color-primary);
  outline-offset: 2px;
}

Keep reading

Want to build something like this?

Tell us what you need: we look at it together and propose a way forward in writing, in English or Spanish.

[email protected]