HTML & CSS: The Document ModelThe DocumentLesson 4 of 18

Accessibility Fundamentals

Accessibility is not an add-on feature or compliance checkbox. It's a design constraint that makes interfaces usable by the widest possible audience, including people using assistive technologies, keyboard navigation, or alternative input devices.

The web platform provides accessibility infrastructure by default. HTML semantics create an accessibility tree that screen readers and other assistive technologies use to navigate and understand your content. Your job is to preserve and enhance that infrastructure, not break it.

The Accessibility Tree

Parallel to the DOM, browsers construct an accessibility tree—a simplified representation of the page structure optimized for assistive technologies.

The accessibility tree contains:

  • Roles (what elements are: button, navigation, heading)
  • States (checked, expanded, disabled)
  • Properties (label, description, level)
  • Relationships (which label describes which input)

Screen readers traverse this tree, announcing elements and their relationships. Semantic HTML elements automatically populate the accessibility tree with correct roles and properties.

<!-- Semantic HTML creates accessibility tree nodes -->
<button>Save</button>
<!-- Role: button, Label: "Save" -->

<nav>
  <ul>
    <li><a href="/">Home</a></li>
  </ul>
</nav>
<!-- Role: navigation, contains list with links -->

<input type="checkbox" id="terms">
<label for="terms">I agree</label>
<!-- Role: checkbox, Label: "I agree" -->

When you use <div> and <span> without semantic meaning, these elements provide no accessibility information. The accessibility tree contains empty nodes that assistive technologies must skip or guess about.

ARIA: Accessible Rich Internet Applications

ARIA provides attributes to expose roles, states, and properties when semantic HTML is insufficient or impossible.

ARIA has three categories:

  • Roles: Define what something is (role="button", role="dialog")
  • States: Define current condition (aria-checked="true", aria-expanded="false")
  • Properties: Define characteristics (aria-label="Close", aria-describedby="help-text")

The First Rule of ARIA

Use semantic HTML first. ARIA does not change behavior—it only changes how assistive technologies interpret elements.

<!-- WRONG: div with ARIA -->
<div role="button" tabindex="0" onclick="save()">Save</div>

<!-- RIGHT: semantic HTML -->
<button onclick="save()">Save</button>

The <button> element provides:

  • Keyboard interaction (Space and Enter activate it)
  • Focus management (Tab stops on it)
  • Accessibility role (announced as "button")
  • Visual affordances (default button styling)

The <div> with role="button" only provides the role announcement. You must implement keyboard handling, focus styles, and interaction patterns manually—all of which the <button> element provides automatically.

Landmark Roles vs Semantic HTML

ARIA landmark roles define major page regions. Semantic HTML5 elements create these landmarks automatically:

HTML ElementImplicit ARIA Role
<header>banner (when not nested)
<nav>navigation
<main>main
<aside>complementary
<footer>contentinfo (when not nested)
<section>region (when labeled)
<form>form (when labeled)

Use the HTML element, not the ARIA role:

<!-- WRONG: div with ARIA role -->
<div role="navigation">
  <a href="/">Home</a>
</div>

<!-- RIGHT: semantic HTML -->
<nav>
  <a href="/">Home</a>
</nav>

Screen reader users can navigate by landmarks—jumping directly from navigation to main content to footer. This only works when landmarks exist in the accessibility tree.

Labeling Elements

aria-label

Provides a text label directly on an element. Screen readers announce this instead of the element's content.

<button aria-label="Close dialog">
  <svg><!-- X icon --></svg>
</button>

Without aria-label, screen readers announce nothing—the SVG icon has no text content. The label makes the button's purpose clear.

Always provide accessible labels on icon-only buttons. Visual users see the icon. Screen reader users need the text equivalent.

<!-- Icon buttons MUST have aria-label -->
<button aria-label="Search">
  <svg><!-- search icon --></svg>
</button>

<button aria-label="Menu">
  <svg><!-- hamburger icon --></svg>
</button>

<button aria-label="Favorite this item">
  <svg><!-- heart icon --></svg>
</button>

aria-labelledby

References another element's text as the label. Useful when the label exists elsewhere in the DOM.

<div role="dialog" aria-labelledby="dialog-title">
  <h2 id="dialog-title">Confirm Deletion</h2>
  <p>Are you sure you want to delete this item?</p>
  <button>Delete</button>
  <button>Cancel</button>
</div>

Screen readers announce "Confirm Deletion, dialog" when the dialog opens, using the heading text as the dialog's label.

aria-describedby

References additional descriptive text. Unlike labels (which replace content), descriptions supplement it.

<input
  type="password"
  id="password"
  aria-describedby="password-hint">
<label for="password">Password</label>
<p id="password-hint">Must be at least 8 characters with one number.</p>

Screen readers announce: "Password, edit text, Must be at least 8 characters with one number."

The label identifies the field. The description provides additional context.

Focus Management

Keyboard users navigate with Tab (forward) and Shift+Tab (backward). Focus moves through interactive elements in source order.

Tab Order

Only interactive elements should receive focus:

  • Links (<a> with href)
  • Buttons (<button>, <input type="button">)
  • Form inputs (<input>, <textarea>, <select>)
  • Elements with tabindex="0"

Non-interactive elements (headings, paragraphs, images) should not be in the tab order. Users navigate these with screen reader commands, not Tab.

<!-- WRONG: div in tab order -->
<div tabindex="0" class="card">
  <h3>Card Title</h3>
  <p>Card content</p>
</div>

<!-- RIGHT: only interactive elements focusable -->
<div class="card">
  <h3>Card Title</h3>
  <p>Card content</p>
  <button>Learn More</button>
</div>

Use tabindex="-1" to make something programmatically focusable without adding it to tab order. This is useful for headings you want to focus with JavaScript after route changes.

Never use tabindex values greater than 0. They break natural tab order and create confusion.

Focus Visibility

Users must see which element has focus. The default browser focus outline is functional but often visually clashing. Customize it while maintaining visibility:

/* Remove default outline */
:focus {
  outline: none;
}

/* Add custom focus indicator */
:focus-visible {
  outline: 2px solid #3b82f6;
  outline-offset: 2px;
}

:focus-visible shows focus for keyboard users but not for mouse clicks, matching user expectations.

Scrolling Focused Elements Into View

When focus moves to an element outside the viewport, scroll it into view:

element.focus();
element.scrollIntoView({ behavior: 'smooth', block: 'nearest' });

This is critical for:

  • Skip links that jump to main content
  • Keyboard shortcuts that focus specific sections
  • Form validation that focuses the first invalid field
  • Search results that highlight and focus matches

Without scrollIntoView(), focus moves but the viewport doesn't—keyboard users lose their position.

Managing Focus in Modals

When a modal opens, focus should move inside it. When it closes, focus should return to the trigger element.

const openButton = document.querySelector('[data-open-modal]');
const modal = document.querySelector('[role="dialog"]');
const closeButton = modal.querySelector('[data-close]');

// Open modal
openButton.addEventListener('click', () => {
  modal.hidden = false;

  // Move focus into modal
  closeButton.focus();

  // Store trigger for return focus
  modal.dataset.trigger = openButton;
});

// Close modal
closeButton.addEventListener('click', () => {
  modal.hidden = true;

  // Return focus to trigger
  const trigger = document.querySelector(`[data-open-modal="${modal.dataset.trigger}"]`);
  if (trigger) {
    trigger.focus();
  }
});

This focus loop keeps keyboard users inside the modal until they close it. Without it, tabbing reaches elements behind the modal—confusing and broken.

The Inert Attribute

The inert attribute marks content as non-interactive. Inert elements:

  • Cannot receive focus
  • Are excluded from assistive technology
  • Ignore pointer events

Use inert to disable background content when a modal or drawer is open:

<div id="main-content" inert>
  <!-- Background content becomes inert when modal opens -->
</div>

<div role="dialog" aria-modal="true">
  <!-- Modal content remains interactive -->
</div>
// Open modal
mainContent.inert = true;
modal.hidden = false;

// Close modal
modal.hidden = true;
mainContent.inert = false;

This prevents keyboard users from tabbing behind the modal and tells screen readers to ignore inactive content.

Skip Links

Skip links let keyboard users bypass repeated navigation and jump directly to main content:

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

<header>
  <nav>
    <!-- 20+ navigation links -->
  </nav>
</header>

<main id="main" tabindex="-1">
  <!-- Main content -->
</main>
.skip-link {
  position: absolute;
  top: -40px;
  left: 0;
  z-index: 1000;
  padding: 8px;
  background: #000;
  color: #fff;
}

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

The skip link is hidden off-screen until focused. When a keyboard user presses Tab on page load, the skip link appears. Activating it jumps focus to #main, bypassing navigation.

Add tabindex="-1" to the <main> element so it can receive programmatic focus even though it's not interactive.

Color Contrast

Text must have sufficient contrast against its background for readability. WCAG defines minimum contrast ratios:

  • Normal text: 4.5:1 contrast ratio (AA level)
  • Large text (18px+ or 14px+ bold): 3:1 contrast ratio (AA level)
  • Enhanced: 7:1 for normal, 4.5:1 for large (AAA level)

Use browser DevTools or online contrast checkers to verify ratios. Low contrast impacts users with low vision, color blindness, or viewing screens in bright sunlight.

/* FAIL: 2.1:1 contrast ratio */
.text {
  color: #767676;
  background: #ffffff;
}

/* PASS: 4.6:1 contrast ratio */
.text {
  color: #595959;
  background: #ffffff;
}

Avoid relying solely on color to convey information. Add icons, patterns, or text labels:

<!-- WRONG: color only -->
<span style="color: red;">Error</span>
<span style="color: green;">Success</span>

<!-- RIGHT: color + icon + text -->
<span class="error">
  <svg aria-hidden="true"><!-- X icon --></svg>
  Error
</span>
<span class="success">
  <svg aria-hidden="true"><!-- checkmark icon --></svg>
  Success
</span>

Check Your Understanding

What is the first rule of ARIA?

Not quite. The correct answer is highlighted.

When should icon-only buttons have an aria-label?

Not quite. The correct answer is highlighted.
Only elements should be in the tab order for keyboard navigation.
Not quite.Expected: interactive

What does the inert attribute do?

Not quite. The correct answer is highlighted.

What is the minimum contrast ratio for normal text to meet WCAG AA standards?

Not quite. The correct answer is highlighted.
When a modal closes, focus should return to the element that opened it.
Not quite.Expected: trigger

Practice

Summary

  • Accessibility Tree: Browsers create a parallel tree from HTML that assistive technologies use for navigation—semantic HTML populates it automatically.
  • ARIA When Needed: Use semantic HTML first; ARIA only adds roles, states, and properties when HTML elements are insufficient or impossible.
  • Landmark Roles: Semantic elements (<nav>, <main>, <aside>) create landmarks automatically—use elements, not ARIA roles.
  • Icon Button Labels: Icon-only buttons must have aria-label to provide text alternatives for screen readers—visual icons alone are insufficient.
  • Focus Management: Only interactive elements should be in tab order; use tabindex="-1" for programmatic focus without adding to tab sequence.
  • Scroll Into View: When programmatically focusing elements, call scrollIntoView() to ensure keyboard users can see focused elements.
  • Modal Focus Loop: When modals open, move focus inside; when closed, return focus to the trigger element to maintain keyboard user orientation.
  • Inert Attribute: Mark background content as inert when modals or drawers are open to prevent focus and interaction with inactive content.
  • Skip Links: Provide skip links at page top to let keyboard users bypass repeated navigation and jump to main content.
  • Color Contrast: Maintain 4.5:1 contrast for normal text, 3:1 for large text; never rely on color alone to convey information.