Steven's Knowledge
Accessibility

Semantic HTML

Document structure, landmarks, headings, and native HTML semantics for accessibility

Semantic HTML

Semantic HTML is the foundation of accessibility. Using the right elements provides built-in keyboard support, screen reader semantics, and reduces the need for ARIA.

Why Semantics Matter

Native <button> vs <div onClick>
├── <button>
│   ├── ✓ Focusable by default
│   ├── ✓ Activated by Enter and Space
│   ├── ✓ Announced as "button" by screen readers
│   ├── ✓ Can be disabled natively
│   └── ✓ Included in tab order
└── <div onClick>
    ├── ✗ Not focusable
    ├── ✗ No keyboard activation
    ├── ✗ Announced as generic text
    ├── ✗ No disabled state
    └── ✗ Requires tabIndex, role, onKeyDown

Document Structure

Page Skeleton

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Page Title — Site Name</title>
</head>
<body>
  <a href="#main-content" class="skip-link">Skip to main content</a>

  <header>
    <nav aria-label="Main navigation">
      <!-- Primary navigation -->
    </nav>
  </header>

  <main id="main-content">
    <h1>Page Heading</h1>
    <!-- Primary content -->
  </main>

  <aside aria-label="Related content">
    <!-- Sidebar content -->
  </aside>

  <footer>
    <nav aria-label="Footer navigation">
      <!-- Footer links -->
    </nav>
  </footer>
</body>
</html>

Skip links allow keyboard users to bypass repetitive navigation.

.skip-link {
  position: absolute;
  top: -40px;
  left: 0;
  padding: 8px 16px;
  background: #000;
  color: #fff;
  z-index: 100;
  transition: top 0.2s;
}

.skip-link:focus {
  top: 0;
}
// React skip link component
function SkipLink({ targetId, children = 'Skip to main content' }: SkipLinkProps) {
  return (
    <a href={`#${targetId}`} className="skip-link">
      {children}
    </a>
  );
}

// Usage in layout
function Layout({ children }: LayoutProps) {
  return (
    <>
      <SkipLink targetId="main-content" />
      <Header />
      <main id="main-content" tabIndex={-1}>
        {children}
      </main>
      <Footer />
    </>
  );
}

Landmark Elements

Landmarks provide structural navigation for screen reader users.

HTML ElementARIA RolePurpose
<header>bannerSite-level header (when not nested)
<nav>navigationNavigation links
<main>mainPrimary page content (one per page)
<aside>complementarySupporting content
<footer>contentinfoSite-level footer (when not nested)
<section>regionThematic grouping (when labeled)
<form>formWhen given an accessible name
<search>searchSearch functionality
// Label multiple navigation landmarks
function AppLayout() {
  return (
    <>
      <header>
        <nav aria-label="Main">
          <ul>
            <li><a href="/">Home</a></li>
            <li><a href="/products">Products</a></li>
          </ul>
        </nav>
      </header>

      <main>
        <h1>Products</h1>
        {/* content */}
      </main>

      <aside aria-label="Filters">
        {/* sidebar filters */}
      </aside>

      <footer>
        <nav aria-label="Footer">
          <ul>
            <li><a href="/privacy">Privacy</a></li>
            <li><a href="/terms">Terms</a></li>
          </ul>
        </nav>
      </footer>
    </>
  );
}

Label duplicate landmarks. When you have multiple elements of the same landmark type (e.g., multiple <nav>), give each a unique aria-label so screen reader users can distinguish them.

Heading Hierarchy

Headings create an outline that screen reader users navigate by. Maintain a logical hierarchy.

Correct Heading Structure
├── h1: Page Title (one per page)
│   ├── h2: Section A
│   │   ├── h3: Subsection A.1
│   │   └── h3: Subsection A.2
│   ├── h2: Section B
│   │   ├── h3: Subsection B.1
│   │   │   └── h4: Detail B.1.1
│   │   └── h3: Subsection B.2
│   └── h2: Section C
// Bad - skipping heading levels
<h1>Dashboard</h1>
<h4>Recent Orders</h4>  {/* Skipped h2, h3 */}

// Good - logical hierarchy
<h1>Dashboard</h1>
<h2>Recent Orders</h2>

// Bad - using headings for styling
<h3 className="small-text">Not really a heading</h3>

// Good - use CSS for visual styling, headings for structure
<p className="section-label">Not really a heading</p>

Native Interactive Elements

// Button — triggers an action
<button onClick={handleSave}>Save</button>
<button onClick={handleDelete} type="button">Delete</button>

// Link — navigates to a destination
<a href="/settings">Settings</a>
<a href="/report.pdf" download>Download Report</a>

// Bad - link that acts as a button
<a href="#" onClick={handleSave}>Save</a>

// Bad - div pretending to be a button
<div className="btn" onClick={handleSave}>Save</div>
Use CaseElementReason
Triggers an action (save, delete, toggle)<button>Built-in keyboard support and button role
Navigates to a URL<a href>Provides link context, right-click menu, open in new tab
Submits a form<button type="submit">Triggers form submission on Enter
Opens an external link<a href="..." target="_blank" rel="noopener noreferrer">Indicates external navigation

Lists

// Navigation as list — screen readers announce item count
<nav aria-label="Main">
  <ul>
    <li><a href="/">Home</a></li>
    <li><a href="/about">About</a></li>
    <li><a href="/contact">Contact</a></li>
  </ul>
</nav>
// Screen reader: "navigation, Main, list, 3 items"

// Definition list for key-value pairs
<dl>
  <dt>Status</dt>
  <dd>Active</dd>
  <dt>Role</dt>
  <dd>Administrator</dd>
</dl>

Tables

// Data table with proper semantics
<table>
  <caption>Quarterly Sales Report</caption>
  <thead>
    <tr>
      <th scope="col">Quarter</th>
      <th scope="col">Revenue</th>
      <th scope="col">Growth</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th scope="row">Q1 2024</th>
      <td>$1.2M</td>
      <td>+15%</td>
    </tr>
    <tr>
      <th scope="row">Q2 2024</th>
      <td>$1.5M</td>
      <td>+25%</td>
    </tr>
  </tbody>
</table>
AttributePurpose
<caption>Visible table description
scope="col"Header applies to column
scope="row"Header applies to row
headers="id1 id2"For complex tables with multi-level headers

Language and Text

<!-- Set page language -->
<html lang="en">

<!-- Mark language changes inline -->
<p>The French word <span lang="fr">bonjour</span> means hello.</p>

<!-- Abbreviations -->
<abbr title="Web Content Accessibility Guidelines">WCAG</abbr>

<!-- Text direction for RTL content -->
<p dir="rtl" lang="ar">مرحبا</p>

Best Practices

Semantic HTML Guidelines

  1. Use native HTML elements before reaching for ARIA
  2. Maintain a single <h1> and logical heading hierarchy per page
  3. Include skip links for keyboard navigation
  4. Label duplicate landmarks with unique aria-label
  5. Use <button> for actions, <a> for navigation
  6. Provide <caption> and scope attributes for data tables
  7. Set the lang attribute on <html> and on inline language changes
  8. Use <ul>/<ol> for lists — screen readers announce item count

On this page