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, onKeyDownDocument 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
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 Element | ARIA Role | Purpose |
|---|---|---|
<header> | banner | Site-level header (when not nested) |
<nav> | navigation | Navigation links |
<main> | main | Primary page content (one per page) |
<aside> | complementary | Supporting content |
<footer> | contentinfo | Site-level footer (when not nested) |
<section> | region | Thematic grouping (when labeled) |
<form> | form | When given an accessible name |
<search> | search | Search 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
Buttons vs Links
// 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 Case | Element | Reason |
|---|---|---|
| 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>| Attribute | Purpose |
|---|---|
<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
- Use native HTML elements before reaching for ARIA
- Maintain a single
<h1>and logical heading hierarchy per page - Include skip links for keyboard navigation
- Label duplicate landmarks with unique
aria-label - Use
<button>for actions,<a>for navigation - Provide
<caption>andscopeattributes for data tables - Set the
langattribute on<html>and on inline language changes - Use
<ul>/<ol>for lists — screen readers announce item count