View contents
Semantic HTML Implementation Guide: Complete Patterns for Accessible Structure
Introduction
Semantic HTML is the foundation of accessible web development. This guide covers comprehensive patterns for implementing proper document structure, landmark regions, heading hierarchies, and semantic elements that enable assistive technologies to understand and navigate your content effectively.
Beyond accessibility, semantic HTML improves SEO, code maintainability, and provides better default behaviors than generic elements styled to look similar.
Document Structure Patterns
Complete Page Template
<!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>
<!-- Skip link for keyboard users -->
<a href="#main-content" class="skip-link">Skip to main content</a>
<!-- Site header with logo and navigation -->
<header class="site-header">
<a href="/" class="logo">
<img src="/logo.svg" alt="Company Name">
</a>
<nav aria-label="Main navigation">
<ul>
<li><a href="/">Home</a></li>
<li><a href="/about">About</a></li>
<li><a href="/services">Services</a></li>
<li><a href="/contact">Contact</a></li>
</ul>
</nav>
</header>
<!-- Main content area -->
<main id="main-content" tabindex="-1">
<article>
<header>
<h1>Article Title</h1>
<p class="meta">
Published <time datetime="2024-12-16">December 16, 2024</time>
</p>
</header>
<section aria-labelledby="intro-heading">
<h2 id="intro-heading">Introduction</h2>
<p>Article introduction content...</p>
</section>
<section aria-labelledby="main-heading">
<h2 id="main-heading">Main Section</h2>
<p>Main content...</p>
</section>
</article>
</main>
<!-- Sidebar content -->
<aside aria-label="Related content">
<h2>Related Articles</h2>
<ul>
<li><a href="/article-1">Related Article 1</a></li>
<li><a href="/article-2">Related Article 2</a></li>
</ul>
</aside>
<!-- Site footer -->
<footer class="site-footer">
<nav aria-label="Footer navigation">
<ul>
<li><a href="/privacy">Privacy Policy</a></li>
<li><a href="/terms">Terms of Service</a></li>
</ul>
</nav>
<p>© 2024 Company Name. All rights reserved.</p>
</footer>
</body>
</html>
Landmark Regions
ARIA Landmarks vs HTML5 Elements
| HTML5 Element | ARIA Role | Purpose |
|---|---|---|
<header> (page-level) |
banner |
Site header |
<nav> |
navigation |
Navigation links |
<main> |
main |
Primary content |
<aside> |
complementary |
Related content |
<footer> (page-level) |
contentinfo |
Site footer |
<section> |
region (if labeled) |
Generic section |
<form> |
form (if labeled) |
Form container |
<search> |
search |
Search functionality |
When to Add ARIA Labels
<!-- Multiple nav elements need labels -->
<nav aria-label="Main navigation">
<!-- Primary nav links -->
</nav>
<nav aria-label="Footer navigation">
<!-- Footer links -->
</nav>
<!-- Multiple sections should be labeled -->
<section aria-labelledby="features-heading">
<h2 id="features-heading">Features</h2>
<!-- Content -->
</section>
<section aria-labelledby="pricing-heading">
<h2 id="pricing-heading">Pricing</h2>
<!-- Content -->
</section>
<!-- Search landmark -->
<search>
<form role="search" action="/search">
<label for="search-input">Search</label>
<input type="search" id="search-input" name="q">
<button type="submit">Search</button>
</form>
</search>
Avoiding Landmark Overuse
<!-- BAD: Too many landmarks -->
<main>
<section aria-label="Introduction">
<section aria-label="Paragraph 1">
<p>...</p>
</section>
<section aria-label="Paragraph 2">
<p>...</p>
</section>
</section>
</main>
<!-- GOOD: Meaningful landmarks only -->
<main>
<h1>Page Title</h1>
<p>Introduction paragraph...</p>
<p>Another paragraph...</p>
<section aria-labelledby="features">
<h2 id="features">Features</h2>
<!-- Meaningful section content -->
</section>
</main>
Heading Implementation
Proper Hierarchy
<!-- Level 1: Page title (one per page) -->
<h1>Ultimate Guide to Web Accessibility</h1>
<!-- Level 2: Major sections -->
<h2>Understanding WCAG Guidelines</h2>
<!-- Level 3: Subsections -->
<h3>Perceivable</h3>
<h3>Operable</h3>
<h3>Understandable</h3>
<h3>Robust</h3>
<h2>Implementation Strategies</h2>
<h3>Semantic HTML</h3>
<!-- Level 4: Sub-subsections -->
<h4>Document Structure</h4>
<h4>Content Elements</h4>
<h3>ARIA When Needed</h3>
<h2>Testing Methods</h2>
Heading in Components
<!-- Card component with proper heading level -->
<article class="card">
<h3>Card Title</h3> <!-- Assumes h2 is section title -->
<p>Card description...</p>
<a href="/details">Read more</a>
</article>
<!-- Adjustable heading component (React example concept) -->
<!--
<Card headingLevel={3}>
Card content
</Card>
-->
Hidden Headings for Screen Readers
<!-- Visually hidden heading for section without visible title -->
<section aria-labelledby="results-heading">
<h2 id="results-heading" class="visually-hidden">
Search Results
</h2>
<!-- Search results content -->
</section>
<style>
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
</style>
List Patterns
Unordered Lists
<!-- Navigation menu -->
<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>
<!-- Feature list -->
<ul>
<li>Responsive design</li>
<li>Accessibility compliant</li>
<li>SEO optimized</li>
</ul>
Ordered Lists
<!-- Step-by-step instructions -->
<ol>
<li>Open the settings panel</li>
<li>Navigate to Accessibility</li>
<li>Enable screen reader support</li>
<li>Save your changes</li>
</ol>
<!-- Numbered items where order matters -->
<h3>Top 5 Accessibility Tools</h3>
<ol>
<li>axe DevTools</li>
<li>WAVE</li>
<li>Lighthouse</li>
<li>NVDA</li>
<li>VoiceOver</li>
</ol>
Description Lists
<!-- Glossary or definitions -->
<dl>
<dt>WCAG</dt>
<dd>Web Content Accessibility Guidelines - international standard for web accessibility.</dd>
<dt>ARIA</dt>
<dd>Accessible Rich Internet Applications - specification for enhanced semantics.</dd>
<dt>Screen Reader</dt>
<dd>Assistive technology that converts text to speech for blind users.</dd>
</dl>
<!-- Key-value pairs -->
<dl>
<dt>Name</dt>
<dd>John Doe</dd>
<dt>Email</dt>
<dd>[email protected]</dd>
<dt>Role</dt>
<dd>Accessibility Specialist</dd>
</dl>
Nested Lists
<ul>
<li>
Perceivable
<ul>
<li>Text Alternatives</li>
<li>Time-based Media</li>
<li>Adaptable</li>
<li>Distinguishable</li>
</ul>
</li>
<li>
Operable
<ul>
<li>Keyboard Accessible</li>
<li>Enough Time</li>
<li>Seizures</li>
<li>Navigable</li>
</ul>
</li>
</ul>
Table Patterns
Simple Data Table
<table>
<caption>Monthly Sales Report</caption>
<thead>
<tr>
<th scope="col">Month</th>
<th scope="col">Revenue</th>
<th scope="col">Growth</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">January</th>
<td>$10,000</td>
<td>+5%</td>
</tr>
<tr>
<th scope="row">February</th>
<td>$12,000</td>
<td>+20%</td>
</tr>
</tbody>
</table>
Complex Table with Headers
<table>
<caption>
Quarterly Results by Region
<span class="visually-hidden">
Revenue and growth percentages for Q1-Q4 2024
</span>
</caption>
<thead>
<tr>
<th scope="col" rowspan="2">Region</th>
<th scope="colgroup" colspan="2">Q1 2024</th>
<th scope="colgroup" colspan="2">Q2 2024</th>
</tr>
<tr>
<th scope="col">Revenue</th>
<th scope="col">Growth</th>
<th scope="col">Revenue</th>
<th scope="col">Growth</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">North America</th>
<td>$1.2M</td>
<td>+8%</td>
<td>$1.4M</td>
<td>+12%</td>
</tr>
<tr>
<th scope="row">Europe</th>
<td>$800K</td>
<td>+5%</td>
<td>$900K</td>
<td>+10%</td>
</tr>
</tbody>
</table>
Form Structure
Accessible Form Pattern
<form action="/submit" method="post">
<fieldset>
<legend>Personal Information</legend>
<div class="form-group">
<label for="name">Full Name <span aria-hidden="true">*</span></label>
<input
type="text"
id="name"
name="name"
required
aria-describedby="name-hint"
autocomplete="name"
>
<p id="name-hint" class="hint">Enter your legal name as it appears on ID</p>
</div>
<div class="form-group">
<label for="email">Email Address <span aria-hidden="true">*</span></label>
<input
type="email"
id="email"
name="email"
required
autocomplete="email"
>
</div>
</fieldset>
<fieldset>
<legend>Communication Preferences</legend>
<div class="checkbox-group">
<input type="checkbox" id="newsletter" name="newsletter">
<label for="newsletter">Subscribe to newsletter</label>
</div>
<div class="checkbox-group">
<input type="checkbox" id="updates" name="updates">
<label for="updates">Receive product updates</label>
</div>
</fieldset>
<button type="submit">Submit Form</button>
</form>
Radio Button Groups
<fieldset>
<legend>Select your plan</legend>
<div class="radio-group">
<input type="radio" id="plan-basic" name="plan" value="basic">
<label for="plan-basic">Basic - $9/month</label>
</div>
<div class="radio-group">
<input type="radio" id="plan-pro" name="plan" value="pro">
<label for="plan-pro">Pro - $19/month</label>
</div>
<div class="radio-group">
<input type="radio" id="plan-enterprise" name="plan" value="enterprise">
<label for="plan-enterprise">Enterprise - $49/month</label>
</div>
</fieldset>
Interactive Elements
Buttons vs Links
<!-- Button: Performs an action -->
<button type="button" onclick="openModal()">
Open Settings
</button>
<button type="submit">
Submit Form
</button>
<!-- Link: Navigates to a resource -->
<a href="/settings">
Go to Settings
</a>
<a href="/document.pdf" download>
Download PDF
</a>
<!-- WRONG: Link styled as button for action -->
<a href="#" onclick="submitForm(); return false;">
Submit Form
</a>
Details and Summary
<!-- Native disclosure widget -->
<details>
<summary>What is your return policy?</summary>
<p>You can return any item within 30 days of purchase for a full refund.</p>
</details>
<!-- FAQ pattern -->
<div class="faq">
<details>
<summary>How do I reset my password?</summary>
<p>Click the "Forgot Password" link on the login page...</p>
</details>
<details>
<summary>Can I change my username?</summary>
<p>Yes, go to Settings → Profile → Edit Username...</p>
</details>
</div>
Testing Implementation
Automated Testing
// Playwright test for semantic structure
import { test, expect } from '@playwright/test';
test.describe('Semantic HTML Structure', () => {
test('page has proper landmark structure', async ({ page }) => {
await page.goto('/');
// Check for main landmarks
await expect(page.locator('header')).toBeVisible();
await expect(page.locator('main')).toBeVisible();
await expect(page.locator('footer')).toBeVisible();
// Only one main element
const mainElements = await page.locator('main').count();
expect(mainElements).toBe(1);
});
test('heading hierarchy is correct', async ({ page }) => {
await page.goto('/');
// Get all headings
const headings = await page.locator('h1, h2, h3, h4, h5, h6').all();
let lastLevel = 0;
for (const heading of headings) {
const tagName = await heading.evaluate(el => el.tagName);
const level = parseInt(tagName[1]);
// Should not skip more than one level
if (lastLevel > 0 && level > lastLevel + 1) {
const text = await heading.textContent();
throw new Error(`Heading level skipped: "${text}" is h${level} after h${lastLevel}`);
}
lastLevel = level;
}
});
test('only one h1 per page', async ({ page }) => {
await page.goto('/');
const h1Count = await page.locator('h1').count();
expect(h1Count).toBe(1);
});
test('lists use proper elements', async ({ page }) => {
await page.goto('/');
// Check nav uses ul/ol
const navLists = await page.locator('nav ul, nav ol').count();
const navs = await page.locator('nav').count();
expect(navLists).toBeGreaterThanOrEqual(navs);
});
});
Summary Checklist
- [ ] Page has proper landmark regions (header, nav, main, footer)
- [ ] Only one
<main>element per page - [ ] Only one
<h1>per page (usually) - [ ] Heading hierarchy is logical (no skipped levels)
- [ ] Navigation uses
<nav>with<ul>or<ol> - [ ] Lists use appropriate list elements
- [ ] Tables have proper headers and captions
- [ ] Forms use fieldset and legend for groups
- [ ] Buttons are
<button>, links are<a> - [ ] Multiple landmarks of same type have labels
References
- W3C - WCAG 2.2 SC 1.3.1 Info and Relationships
- W3C - WCAG 2.2 SC 4.1.2 Name, Role, Value
- W3C - ARIA Landmarks Example
- MDN - HTML elements reference
- WebAIM - Semantic Structure