Chapter 1
HTML5 Semantic Elements
How semantic markup builds the accessibility tree, landmarks, sectioning, headings, text-level and interactive semantics.
Introduction: What "Semantic" Means
HTML has two jobs. It tells the browser what to display, and it tells every program that reads the page — screen readers, search engines, browser reader modes, translation tools, voice assistants — what each piece of content is. The second job is what we call semantics.
Compare these two fragments. They can be styled to look identical:
<!-- Version A: non-semantic -->
<div class="top">
<div class="title">Daily Brew</div>
<div class="links">
<div onclick="go('/menu')">Menu</div>
<div onclick="go('/about')">About</div>
</div>
</div>
<!-- Version B: semantic -->
<header>
<p class="title">Daily Brew</p>
<nav aria-label="Primary">
<ul>
<li><a href="/menu">Menu</a></li>
<li><a href="/about">About</a></li>
</ul>
</nav>
</header>Explanation, block by block:
- In Version A, every element is a
<div>. A<div>means "a generic box" and nothing more. A screen reader user hears "Daily Brew, Menu, About" with no indication that this is a site header, that there is navigation, or that "Menu" is clickable. Theonclickhandlers on<div>elements cannot be reached with the Tab key and do not respond to Enter, so keyboard users cannot use them at all. - In Version B,
<header>exposes a banner landmark,<nav>exposes a navigation landmark (labelled "Primary" so it can be distinguished from other navs),<ul>/<li>announce "list, 2 items", and<a href>elements are real links: focusable, activated by Enter, openable in a new tab, and visible to crawlers.
Semantic HTML is therefore not a style preference. It is the foundation of accessibility, SEO, and maintainability — and it is free, because the browser implements the behaviour for you.
Key idea: Choose an element for what the content is, never for how it should look. Appearance is CSS's job.
Learning objectives
By the end of this chapter you will be able to:
- Explain how the browser turns HTML elements into an accessibility tree.
- Structure a full page using landmark elements:
header,nav,main,aside,footer. - Decide correctly between
article,section, anddiv. - Build a logical heading hierarchy.
- Use text-level semantics such as
time,figure,address,abbr,mark,strong, andem. - Use interactive semantic elements:
details/summaryanddialog. - Audit and debug semantics using browser developer tools.
How Browsers Use Semantics: The Accessibility Tree
When the browser parses HTML it builds the DOM (Document Object Model). From the DOM it derives a second structure, the accessibility tree, which is what assistive technologies actually read. Each node in that tree has:
| Property | Meaning | Example |
|---|---|---|
| Role | What kind of thing it is | link, button, heading, navigation |
| Name | Its accessible label | "Menu", "Search articles" |
| State | Its current condition | expanded, checked, disabled |
| Level / value | Extra details for some roles | heading level 2 |
Native HTML elements come with an implicit role. You get it automatically:
<nav> <!-- role: navigation -->
<main> <!-- role: main -->
<button> <!-- role: button, focusable, Enter/Space activate it -->
<h2> <!-- role: heading, level: 2 -->
<ul> <!-- role: list -->
<a href="#"> <!-- role: link (only when href is present!) -->A <div> and <span> have the role generic, which assistive technology largely ignores.
Seeing the tree yourself
- Open any page in Chrome or Edge and press F12.
- In the Elements panel, select an element, then open the Accessibility tab in the side pane.
- Check the "Computed Properties" for Role and Name.
- Enable "Full-page accessibility tree" (the person icon at the top of the Elements panel) to browse the entire tree.
Firefox has an equivalent Accessibility panel in its DevTools that can also list every landmark on the page.
Practical demonstration: Inspect Version A from the introduction. The "Menu" div shows role generic and is not focusable. Now inspect Version B — the link shows role link with name "Menu". This five-second check is the fastest semantic audit you can perform.
The Page Skeleton: Landmark Elements
Landmarks are the large regions of a page. Screen reader users can jump directly between them (for example, the D key in NVDA or the rotor in VoiceOver), the same way sighted users glance at a layout.
The complete skeleton
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Pour-Over Guide — Daily Brew</title>
</head>
<body>
<a class="skip-link" href="#content">Skip to main content</a>
<header>
<a href="/" class="logo">Daily Brew</a>
<nav aria-label="Primary">
<ul>
<li><a href="/guides" aria-current="page">Guides</a></li>
<li><a href="/shop">Shop</a></li>
<li><a href="/about">About</a></li>
</ul>
</nav>
</header>
<main id="content">
<h1>The Pour-Over Guide</h1>
<!-- primary content of this page -->
</main>
<aside aria-labelledby="related-heading">
<h2 id="related-heading">Related guides</h2>
<ul>
<li><a href="/guides/french-press">French press</a></li>
</ul>
</aside>
<footer>
<p>© 2026 Daily Brew</p>
<nav aria-label="Footer">
<ul>
<li><a href="/privacy">Privacy</a></li>
<li><a href="/contact">Contact</a></li>
</ul>
</nav>
</footer>
</body>
</html>Logical-block explanation:
<html lang="en">— declares the language. Screen readers use it to choose pronunciation; browsers use it for hyphenation and translation prompts. Omitting it is one of the most common accessibility failures.<title>— the first thing a screen reader announces and the text shown in browser tabs and search results. Make it specific: "Page — Site", not just "Site".- Skip link — the first focusable element. Keyboard users press Tab once and Enter to jump past the navigation. It is usually visually hidden until focused (see the CSS below).
<header>as a direct child ofbodybecomes the banner landmark. It holds site-wide identity: logo, primary navigation, perhaps search.<nav aria-label="Primary">— a navigation landmark. When a page has more than one<nav>, label each one so users hear "Primary navigation" and "Footer navigation" instead of two identical "navigation" regions.aria-current="page"— tells assistive technology which link represents the current page. It is also a convenient CSS hook:a[aria-current="page"] { font-weight: 700; }.<main id="content">— the main landmark. There must be exactly one visible<main>per page. Itsidis the skip link target.<aside>— a complementary landmark: content related to, but separable from, the main content. Labelling it witharia-labelledbypointing at its heading gives it a name.<footer>as a direct child ofbodybecomes the contentinfo landmark: copyright, legal links, contact.
The skip link CSS:
.skip-link {
position: absolute;
left: 0.5rem;
top: -3rem; /* off-screen by default */
padding: 0.5rem 1rem;
background: #111;
color: #fff;
z-index: 100;
}
.skip-link:focus {
top: 0.5rem; /* slides into view when focused */
}Important nuance:
<header>and<footer>only become page-level landmarks when they are not nested insidearticle,aside,main,nav, orsection. A<header>inside an<article>is simply that article's introductory content. This is correct and expected — you will use it in the next section.
Sectioning Content: article, section, and div
These three elements cause the most confusion. Use this decision procedure:
Could this content be syndicated or make sense on its own,
e.g. in an RSS feed, on another site, or as a standalone card?
├── YES → <article>
└── NO → Is it a thematic group of content that would
naturally have its own heading?
├── YES → <section>
└── NO → <div> (it's just for styling/layout)<article>: self-contained composition
Typical uses: blog posts, news stories, product cards, forum posts, comments, and widgets.
<article class="post">
<header>
<h2><a href="/blog/water-temperature">Why Water Temperature Matters</a></h2>
<p>
By <a href="/authors/ana" rel="author">Ana Ruiz</a>
on <time datetime="2026-03-14">14 March 2026</time>
</p>
</header>
<p>Water between 92 °C and 96 °C extracts flavour evenly...</p>
<footer>
<p>Filed under <a href="/tags/brewing">Brewing</a></p>
</footer>
</article>Explanation:
- The
<article>wraps everything a reader would need if this post appeared alone. - The nested
<header>and<footer>belong to the article, not the page. They are not landmarks here. <time datetime="2026-03-14">exposes a machine-readable date while showing a human-friendly one (covered in detail later).rel="author"describes the relationship between the page and the linked resource.
Articles can nest. A blog post <article> can contain user comments, each its own <article>, because each comment is a self-contained composition related to the outer one.
<section>: thematic grouping with a heading
<main>
<h1>Espresso Machine X200</h1>
<section aria-labelledby="specs">
<h2 id="specs">Specifications</h2>
<dl>
<dt>Pressure</dt><dd>15 bar</dd>
<dt>Water tank</dt><dd>1.8 L</dd>
</dl>
</section>
<section aria-labelledby="reviews">
<h2 id="reviews">Customer reviews</h2>
<!-- each review is an <article> -->
</section>
</main>Explanation:
- Each
<section>groups a theme and starts with a heading. A section without a heading is a strong sign you wanted a<div>. - A
<section>only becomes a region landmark when it has an accessible name (aria-labelledbyoraria-label). Name only the sections important enough to navigate to; too many landmarks become noise. <dl>,<dt>,<dd>form a description list — the correct semantics for name/value pairs such as specifications, glossaries, and metadata.
<div>: no meaning, and that is fine
<div> is not "bad". It is the right choice when you need a wrapper purely for layout — a flex row, a grid container, a card's inner padding. Problems start only when a <div> replaces an element that has meaning, such as a button, list, or heading.
<!-- Correct: div used only for layout -->
<section aria-labelledby="team">
<h2 id="team">Our team</h2>
<div class="grid"> <!-- layout wrapper: no meaning needed -->
<article class="person">...</article>
<article class="person">...</article>
</div>
</section>Headings and Document Outline
Headings (h1–h6) are the primary way screen reader users skim a page: surveys of screen reader users (WebAIM) repeatedly find that navigating by headings is the most common way to find information. Headings form a table of contents for the page.
Rules
- Use one
<h1>describing the page's main topic (usually inside<main>). - Do not skip levels going down: an
<h2>may be followed by<h3>, but not<h4>. Going back up (h4→h2) is fine. - Choose the level by position in the hierarchy, never by font size. If an
<h3>looks too large, change the CSS. - Do not use headings for text that is merely bold or large (taglines, "Call us now!"). Use
<p>with a class.
A well-formed outline
<h1>Brewing Coffee at Home</h1>
<h2>Equipment</h2>
<h3>Grinders</h3>
<h3>Kettles</h3>
<h2>Methods</h2>
<h3>Pour-over</h3>
<h4>Bloom phase</h4>
<h3>French press</h3>
<h2>Troubleshooting</h2>(Indentation is shown for clarity only — HTML does not require it.)
The "outline algorithm" myth
Older tutorials claimed that every <section> resets heading levels, so you could use <h1> everywhere. No browser or screen reader ever implemented that outline algorithm, and it was removed from the HTML specification. Always use explicit heading levels that reflect the real hierarchy.
Visual size vs. level
/* Style by class, keep semantic level correct */
.card-title {
font-size: 1.125rem;
font-weight: 600;
}<section aria-labelledby="latest">
<h2 id="latest">Latest guides</h2>
<article>
<h3 class="card-title">Cold brew in 12 hours</h3>
</article>
</section>The card title is an <h3> because it sits beneath an <h2>. Its small size comes from CSS.
Text-Level Semantics
Inside paragraphs, a handful of elements add precise meaning.
<strong> vs <b>, <em> vs <i>
| Element | Meaning | Example |
|---|---|---|
<strong> |
Strong importance, seriousness, urgency | <strong>Do not</strong> pour boiling water on the glass. |
<em> |
Stress emphasis that changes meaning | I said <em>medium</em> roast. |
<b> |
Draws attention without extra importance | Keywords in a summary, product names |
<i> |
Alternate voice: technical terms, foreign phrases, thoughts | <i lang="it">doppio</i> |
Note lang="it" on the <i>: it tells a screen reader to switch pronunciation for that phrase.
<time>
<p>
Workshop starts
<time datetime="2026-11-03T18:30+01:00">3 November at 6:30 pm</time>
and lasts <time datetime="PT2H">two hours</time>.
</p>- The visible text is for humans;
datetimeis for machines (calendars, search engines, scripts). datetimeaccepts dates (2026-11-03), date-times with time zones (2026-11-03T18:30+01:00), months (2026-11), and durations (PT2H= period of time, 2 hours).
<figure> and <figcaption>
<figure>
<img src="/img/bloom.jpg"
alt="Coffee grounds swelling and releasing bubbles after the first pour"
width="800" height="533">
<figcaption>Figure 2: The bloom phase releases trapped carbon dioxide.</figcaption>
</figure><figure>marks self-contained content referenced from the main text: images, diagrams, code listings, charts, quotations.<figcaption>(first or last child) becomes the figure's accessible name.altdescribes what the image shows; the caption explains why it matters. They should not repeat each other.widthandheightlet the browser reserve space before the image loads, preventing layout shift (see the Web Performance chapter).
Code listings are figures too:
<figure>
<pre><code>const ratio = water / coffee; // 16:1 is typical</code></pre>
<figcaption>Listing 1: Calculating the brew ratio.</figcaption>
</figure><abbr>, <mark>, <address>, <blockquote>, <cite>
<p>
Specialty coffee is graded by the
<abbr title="Specialty Coffee Association">SCA</abbr>.
</p>
<p>Search results for "grind": medium-fine <mark>grind</mark> works best.</p>
<blockquote cite="https://example.com/interview">
<p>Good coffee is ninety percent good water.</p>
</blockquote>
<p>— <cite>The Barista Interview</cite></p>
<address>
Contact the author: <a href="mailto:ana@example.com">ana@example.com</a>
</address><abbr>marks an abbreviation. Expand it in text on first use as well; thetitletooltip is not reliably available to touch or keyboard users.<mark>highlights text relevant in the current context, such as search matches.<blockquote>marks an extended quotation;<cite>marks the title of a work (not the person's name).<address>gives contact information for the nearest<article>or the whole page. It is not for arbitrary postal addresses that are just content (use<p>for those).
Lists and tables
<ul>for unordered items,<ol>for sequences where order matters (recipe steps),<dl>for name/value pairs.<table>for tabular data only — never for layout. Include<caption>and use<th scope="col">/<th scope="row">so cells are announced with their headers.
<table>
<caption>Brew ratios by method</caption>
<thead>
<tr><th scope="col">Method</th><th scope="col">Ratio</th></tr>
</thead>
<tbody>
<tr><th scope="row">Pour-over</th><td>1:16</td></tr>
<tr><th scope="row">French press</th><td>1:12</td></tr>
</tbody>
</table>Interactive Semantic Elements
Buttons vs. links
This is the single most important interactive distinction:
<a href>navigates — to another page, a section, or a file.<button>performs an action on the current page — submit, open a menu, toggle, delete.
<a href="/cart">View cart</a> <!-- goes somewhere -->
<button type="button" id="add">Add to cart</button> <!-- does something -->Always set type="button" on buttons inside forms that should not submit; the default type is submit.
What you lose by faking a button with a div:
<!-- Wrong -->
<div class="btn" onclick="addToCart()">Add to cart</div>To repair it you would need role="button", tabindex="0", a keydown handler for Enter and Space, focus styles, and disabled-state handling. The native <button> provides all of it for free. The first rule of ARIA is: don't use ARIA if a native element does the job.
<details> and <summary>
A disclosure widget with zero JavaScript:
<details>
<summary>What grind size should I use?</summary>
<p>Medium-fine, similar to table salt, for most pour-over drippers.</p>
</details><summary>is focusable, toggles on Enter/Space, and is announced with its expanded/collapsed state.- Add the
openattribute to start expanded. - Giving several
<details>the samenameattribute (supported in current browsers) makes them an exclusive accordion where opening one closes the others.
<dialog>
<button type="button" id="open-help">Brewing help</button>
<dialog id="help" aria-labelledby="help-title">
<h2 id="help-title">Brewing help</h2>
<p>Start with 15 g coffee and 250 g water.</p>
<form method="dialog">
<button>Close</button>
</form>
</dialog>
<script>
const dialog = document.getElementById("help");
document.getElementById("open-help").addEventListener("click", () => {
dialog.showModal();
});
</script>Explanation:
showModal()opens the dialog as modal: the rest of the page becomes inert, focus moves into the dialog, and Escape closes it — behaviours that are notoriously hard to build correctly by hand.<form method="dialog">closes the dialog when its button is pressed, with no extra script.- When the dialog closes, browsers return focus to the element that opened it.
- Use
dialog.show()(without "Modal") only for non-modal panels.
Putting It Together: A Complete Semantic Page
The following page combines everything in this chapter. Read it top to bottom and identify the role of each element before reading the explanation.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Why Water Temperature Matters — Daily Brew Blog</title>
<meta name="description" content="How water temperature affects coffee extraction, with a practical guide.">
</head>
<body>
<a class="skip-link" href="#content">Skip to main content</a>
<header>
<a href="/" class="logo">Daily Brew</a>
<nav aria-label="Primary">
<ul>
<li><a href="/guides">Guides</a></li>
<li><a href="/blog" aria-current="page">Blog</a></li>
<li><a href="/shop">Shop</a></li>
</ul>
</nav>
<search>
<form action="/search" role="search">
<label for="q">Search the blog</label>
<input id="q" name="q" type="search">
<button>Search</button>
</form>
</search>
</header>
<main id="content">
<article>
<header>
<h1>Why Water Temperature Matters</h1>
<p>
By <a href="/authors/ana" rel="author">Ana Ruiz</a> ·
<time datetime="2026-03-14">14 March 2026</time> ·
6 min read
</p>
</header>
<p>
Temperature controls <strong>how fast</strong> compounds dissolve from
the grounds. Too cool and coffee tastes sour; too hot and it turns bitter.
</p>
<section aria-labelledby="ideal">
<h2 id="ideal">The ideal range</h2>
<p>Most guides recommend 92–96 °C.</p>
<figure>
<table>
<caption>Effect of temperature on taste</caption>
<thead>
<tr><th scope="col">Temperature</th><th scope="col">Typical result</th></tr>
</thead>
<tbody>
<tr><th scope="row">85 °C</th><td>Sour, thin</td></tr>
<tr><th scope="row">94 °C</th><td>Balanced</td></tr>
<tr><th scope="row">100 °C</th><td>Harsh, bitter</td></tr>
</tbody>
</table>
<figcaption>Results vary by roast level.</figcaption>
</figure>
</section>
<section aria-labelledby="measure">
<h2 id="measure">Measuring without a thermometer</h2>
<ol>
<li>Bring water to a full boil.</li>
<li>Remove from heat.</li>
<li>Wait about 30 seconds before pouring.</li>
</ol>
</section>
<section aria-labelledby="faq">
<h2 id="faq">Frequently asked questions</h2>
<details>
<summary>Does this apply to cold brew?</summary>
<p>No. Cold brew relies on time instead of heat.</p>
</details>
</section>
<footer>
<p>Filed under <a href="/tags/brewing">Brewing</a></p>
</footer>
</article>
<section aria-labelledby="comments-heading">
<h2 id="comments-heading">Comments</h2>
<article>
<header>
<h3>Sam</h3>
<p><time datetime="2026-03-15T09:12">15 March, 09:12</time></p>
</header>
<p>Waiting 30 seconds fixed my bitter coffee. Thanks!</p>
</article>
</section>
</main>
<footer>
<p>© 2026 Daily Brew</p>
<address>Questions? <a href="mailto:hello@dailybrew.example">hello@dailybrew.example</a></address>
</footer>
</body>
</html>Logical-block explanation:
- Head —
lang, a specific<title>, and adescriptionmeta tag used by search result snippets. - Site header — banner landmark containing the logo, the labelled primary navigation, and a search form.
<search>— a newer element that exposes the search landmark. Therole="search"on the form is a fallback for older assistive technology; in current browsers either alone is sufficient. The<label for="q">gives the input an accessible name — a placeholder is not a substitute.<main>— the single main landmark, target of the skip link.- Blog post
<article>— self-contained, with its own<header>(title, author, date) and<footer>(tags). Its<h1>is the page's onlyh1. - Named
<section>s — each has an<h2>and is labelled by it, so users can jump to "The ideal range" region. - Table inside a figure — real tabular data, with a caption and header cells scoped to columns and rows.
<ol>— steps where order matters.<details>— FAQ answer hidden until requested, accessible without JavaScript.- Comments section — outside the post article (comments are not part of the author's composition), each comment an
<article>with an<h3>because it sits under the "Comments"<h2>. - Site footer — contentinfo landmark with contact
<address>.
The resulting landmark list a screen reader would present:
banner
navigation "Primary"
search
main
region "The ideal range"
region "Measuring without a thermometer"
region "Frequently asked questions"
region "Comments"
contentinfoCommon Mistakes
1. Div and span soup
<!-- Mistake --> <div class="heading">Our menu</div>
<!-- Fix --> <h2>Our menu</h2>Why it matters: the text is invisible to heading navigation and search engines cannot tell it is a heading.
2. Clickable non-interactive elements
<!-- Mistake --> <span onclick="openMenu()">Menu</span>
<!-- Fix --> <button type="button" aria-expanded="false" aria-controls="menu">Menu</button>3. Links without destinations
<!-- Mistake --> <a href="#" onclick="save()">Save</a>
<!-- Fix --> <button type="button" onclick="save()">Save</button>href="#" scrolls to the top, adds # to the URL, and announces "link" for something that is really an action.
4. Choosing headings by size
<!-- Mistake: jumps from h1 to h4 because h4 "looked right" -->
<h1>Shop</h1>
<h4>New arrivals</h4>
<!-- Fix -->
<h1>Shop</h1>
<h2 class="small-heading">New arrivals</h2>5. Multiple or missing <main>
Every page needs exactly one visible <main>. Single-page applications often forget it because each view renders its own wrapper <div>.
6. Wrapping everything in <section>
A <section> with no heading adds nothing. If you cannot think of a heading for it, use <div>.
7. Placeholder used as label
<!-- Mistake --> <input type="email" placeholder="Email">
<!-- Fix -->
<label for="email">Email</label>
<input id="email" type="email" autocomplete="email">Placeholders disappear when typing, often have poor contrast, and are not consistently announced.
8. Redundant or conflicting ARIA
<!-- Mistake: role duplicates the implicit role -->
<nav role="navigation">...</nav>
<!-- Worse: role overrides correct semantics -->
<button role="link">Next</button>Native semantics are already present; adding roles either does nothing or breaks things.
9. Images without proper alt
- Informative images need a description:
alt="Chart showing sales doubling in March". - Purely decorative images need an empty alt (
alt="") so they are skipped. A missing alt causes screen readers to read the file name, such as "IMG_4021 dot jpeg".
Debugging and Auditing Semantics
Work through this checklist on every page you build.
Step 1 — Validate the markup
Paste your HTML into the W3C Nu HTML Checker (validator.w3.org/nu). It catches unclosed tags, invalid nesting (e.g., a <div> inside a <p>, which the parser silently "fixes" by closing the paragraph early), and duplicate ids that break aria-labelledby.
Step 2 — Inspect landmarks and headings
- Chrome/Edge DevTools → Elements → Accessibility pane for individual nodes.
- Firefox DevTools → Accessibility panel → check issues for "Keyboard" and "Text labels".
- Browser extensions such as axe DevTools, WAVE, or HeadingsMap list the heading outline and landmarks visually.
You can also print the outline from the Console:
// Paste into the DevTools Console to print the heading outline
document.querySelectorAll("h1, h2, h3, h4, h5, h6").forEach((h) => {
const level = Number(h.tagName[1]);
console.log(`${" ".repeat(level - 1)}${h.tagName}: ${h.textContent.trim()}`);
});Explanation: querySelectorAll returns the headings in document order. h.tagName[1] reads the digit from "H2", and repeat indents by level so skipped levels are immediately visible.
// List landmarks and count <main> elements
const selectors = "header, nav, main, aside, footer, section[aria-label], section[aria-labelledby], search, [role]";
document.querySelectorAll(selectors).forEach((el) => {
console.log(el.tagName.toLowerCase(), el.getAttribute("aria-label") ?? "");
});
console.log("main elements:", document.querySelectorAll("main").length);Step 3 — Use only the keyboard
Unplug the mouse (or don't touch it) and press Tab, Shift+Tab, Enter, Space, and Escape:
- Can you reach every interactive control?
- Is the focus indicator always visible?
- Does focus order follow the visual reading order?
- Does the skip link appear and work?
If something is clickable but not reachable, it is almost always a <div> or <span> that should be a <button> or <a href>.
Step 4 — Listen with a screen reader
Spend five minutes with a real screen reader: NVDA (free, Windows), VoiceOver (built in, macOS/iOS: Cmd+F5), or TalkBack (Android). Navigate by headings and landmarks. You will find problems no automated tool reports.
Step 5 — Run an automated audit
Lighthouse (DevTools → Lighthouse → Accessibility) and axe catch roughly a third to a half of accessibility issues — useful, but a perfect score does not mean the page is accessible. Combine automated and manual testing.
Best Practices Summary
- Start every page from the landmark skeleton: skip link,
header,nav,main,footer. - Set
langon<html>and a unique, descriptive<title>. - Exactly one
<main>and normally one<h1>. - Heading levels reflect hierarchy; never skip levels going down.
<article>for self-contained content,<section>for headed thematic groups,<div>for layout.- Links navigate; buttons act. Never put click handlers on non-interactive elements.
- Label every form control with a
<label>. - Label repeated landmarks (
nav, namedsection,aside). - Prefer native elements (
details,dialog,button) over ARIA-rebuilt widgets. - Write meaningful
alttext, oralt=""for decorative images. - Validate, test with the keyboard, and listen with a screen reader before shipping.
Exercises
Attempt each exercise before opening the solutions in the next section.
Exercise 1 — Identify the element
Choose the most appropriate element for each item:
- A site-wide list of links to the main areas of a website.
- A single product card in a grid of products.
- The "Shipping information" part of a product page, with a heading.
- A wrapper used only to create a two-column flex layout.
- The publication date of a news article.
- A control that opens a modal window.
- An expandable "Show ingredients" panel.
Exercise 2 — Refactor div soup
Rewrite the following markup using semantic elements. Keep the classes if you like.
<div class="page">
<div class="top">
<div class="logo">Bean There</div>
<div class="menu">
<div class="item" onclick="location='/'">Home</div>
<div class="item" onclick="location='/events'">Events</div>
</div>
</div>
<div class="content">
<div class="big">Upcoming Events</div>
<div class="event">
<div class="event-title">Latte Art Workshop</div>
<div class="date">Saturday 7 November, 10:00</div>
<div>Learn to pour hearts and rosettas.</div>
<div class="btn" onclick="book(1)">Book a place</div>
</div>
</div>
<div class="bottom">© 2026 Bean There</div>
</div>Exercise 3 — Fix the heading outline
Find and fix every problem in this outline:
<h1>Bean There Café</h1>
<h1>Our Menu</h1>
<h4>Hot drinks</h4>
<h4>Cold drinks</h4>
<h2>Opening hours</h2>
<h5>Holiday hours</h5>
<div class="h2">Find us</div>Exercise 4 — Build an accessible FAQ
Create a FAQ section with a heading and three questions, using no JavaScript. Make it a named region and make opening one answer close the others.
Exercise 5 — Debugging challenge
A teammate reports: "Screen reader users say our site has two main areas, can't find the navigation, and the Sign in control does nothing with the keyboard." Their markup:
<body>
<main>
<div class="nav">
<a href="/">Home</a> <a href="/pricing">Pricing</a>
</div>
<span class="signin" onclick="openLogin()">Sign in</span>
</main>
<main>
<h1>Pricing</h1>
</main>
</body>Explain each cause and write the corrected markup.
Solutions
Solution 1
<nav>(labelled, e.g.,aria-label="Primary", if more than one nav exists) containing a<ul>of<a href>links.<article>— a product card is self-contained.<section>with an<h2>(optionally named witharia-labelledby).<div>— layout only.<time datetime="2026-11-07">.<button type="button">— it performs an action; it does not navigate.<details>with a<summary>Show ingredients</summary>.
Solution 2
<body>
<a class="skip-link" href="#content">Skip to main content</a>
<header class="top">
<a href="/" class="logo">Bean There</a>
<nav class="menu" aria-label="Primary">
<ul>
<li><a class="item" href="/">Home</a></li>
<li><a class="item" href="/events" aria-current="page">Events</a></li>
</ul>
</nav>
</header>
<main id="content" class="content">
<h1 class="big">Upcoming Events</h1>
<article class="event">
<h2 class="event-title">Latte Art Workshop</h2>
<p class="date">
<time datetime="2026-11-07T10:00">Saturday 7 November, 10:00</time>
</p>
<p>Learn to pour hearts and rosettas.</p>
<button type="button" class="btn" onclick="book(1)">
Book a place<span class="visually-hidden"> at Latte Art Workshop</span>
</button>
</article>
</main>
<footer class="bottom">
<p>© 2026 Bean There</p>
</footer>
</body>What changed and why:
- The outer
div.pageis removed;bodyalready serves as the container. (Keep a wrapper div if your CSS needs one — that is a legitimate layout use.) - Navigation items become real links inside a list, so they are focusable, announced as "list, 2 items", and work with middle-click.
- "Upcoming Events" is the page
<h1>; each event is an<article>with an<h2>. - The date uses
<time>with a machine-readable value. - "Book a place" performs an action, so it is a
<button>. The visually hidden text makes the name unique ("Book a place at Latte Art Workshop") when many events list identical buttons. A typical.visually-hiddenclass:
.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;
}Solution 3
Problems: two <h1>s; jump from h1 to h4; jump from h2 to h5; "Find us" is a heading styled with a div.
<h1>Bean There Café</h1>
<h2>Our Menu</h2>
<h3>Hot drinks</h3>
<h3>Cold drinks</h3>
<h2>Opening hours</h2>
<h3>Holiday hours</h3>
<h2>Find us</h2>(An acceptable alternative keeps "Bean There Café" as the logo text in the header as a <p> or link and uses <h1>Our Menu</h1> on a menu page. Either way, there is one <h1> and no skipped levels.)
Solution 4
<section aria-labelledby="faq-title">
<h2 id="faq-title">Frequently asked questions</h2>
<details name="faq">
<summary>Do you offer plant-based milk?</summary>
<p>Yes: oat, almond, and soy at no extra charge.</p>
</details>
<details name="faq">
<summary>Can I bring my laptop?</summary>
<p>Of course. Free Wi-Fi is available; ask staff for the password.</p>
</details>
<details name="faq">
<summary>Do you take reservations?</summary>
<p>Only for groups of six or more. Call us a day in advance.</p>
</details>
</section>aria-labelledbynames the section, making it a region landmark called "Frequently asked questions".- The shared
name="faq"creates exclusive behaviour in supporting browsers. In older browsers the panels simply open independently — a safe fallback, which is an example of progressive enhancement.
Solution 5
Causes:
- "Two main areas" — there are two
<main>elements. Only one visible<main>is allowed. - "Can't find navigation" — the links are in a
<div class="nav">, which has no navigation role. They are also inside<main>, while site navigation belongs in the header. - "Sign in does nothing" — a
<span>withonclickis not focusable and ignores Enter/Space.
Corrected markup:
<body>
<header>
<nav aria-label="Primary">
<ul>
<li><a href="/">Home</a></li>
<li><a href="/pricing" aria-current="page">Pricing</a></li>
</ul>
</nav>
<button type="button" class="signin" onclick="openLogin()">Sign in</button>
</header>
<main>
<h1>Pricing</h1>
</main>
</body>If "Sign in" took users to a separate /login page instead of opening a dialog, it should be <a href="/login">Sign in</a> — the choice follows whether it navigates or acts.
Chapter Summary and Next Steps
- Semantics describe what content is, producing an accessibility tree that assistive technology, search engines, and tools rely on.
- Landmarks (
header,nav,main,aside,footer,search, namedsection) give a page its navigable regions. articleis self-contained,sectionis a headed theme,divis layout.- Headings form the page's table of contents: one
h1, no skipped levels, size via CSS. - Text-level elements (
time,figure,abbr,strong,em,dl,table) add precise meaning. - Native interactive elements (
a,button,details,dialog) provide keyboard and screen reader behaviour for free. - Verify with the validator, DevTools accessibility tree, keyboard, screen reader, and automated audits.
Next: the Forms and Validation chapter builds on these foundations with labels, fieldsets, input types, and accessible error messages. The Web Accessibility chapter later revisits ARIA in depth — only after you have learned to need it less.