Headings, landmarks & semantic structure

A web page has a structure whether or not you mark it up. Sighted users perceive it instantly through size, weight, spacing, and position. Assistive technology perceives only what the HTML declares. Use a heading element and the page gains an entry in the heading list; use a landmark element and it gains a region a user can jump to; use a list element and the count is announced up front. Style a <div> to look like any of these and a sighted user is fooled while everyone navigating by structure gets nothing.

This lesson works through the four structural defects behind most “the page is one flat blob” complaints. Each is fixed with the plain semantic HTML you already have — the win is choosing the element that matches the meaning, not adding ARIA on top.

What you’ll learn

How to build a heading outline with real <h1><h6> elements in order; how to give a page navigable regions with <header>, <nav>, <main>, <aside>, and <footer>; how to mark visual lists up as real lists; and how to keep the DOM order matching the meaningful reading order so what’s heard matches what’s seen.

Standards this lesson maps to
Standard Criterion Level What it requires
WCAG 2.2 1.3.1 Info and Relationships A Structure conveyed visually — headings, lists, and groupings — is also conveyed in the markup, not by styling alone.
WCAG 2.2 1.3.2 Meaningful Sequence A When the order of content affects its meaning, a correct reading sequence is available programmatically.
WCAG 2.2 2.4.1 Bypass Blocks A A mechanism — landmarks, headings, or a skip link — lets users bypass blocks of content repeated across pages.
WCAG 2.2 2.4.3 Focus Order A Focusable components receive focus in an order that preserves meaning and operability.
WCAG 2.2 2.4.6 Headings and Labels AA Headings and labels describe their topic or purpose; the heading structure is genuine, not visual mimicry.
WCAG 2.2 1.3.6 Identify Purpose AAA The purpose of regions and components can be programmatically determined (landmarks support this).
EN 301 549 9.1.3.1 / 9.2.4.1 (incorporates WCAG) European harmonised standard; references the WCAG A/AA set including structure and bypass criteria.
Section 508 502.3 / 504 (incorporates WCAG A & AA) US federal ICT must meet WCAG 2.0 Level A and AA, including info-and-relationships and bypass blocks.
ADA Title II WCAG 2.1 AA (DOJ rule) AA US state/local government web content must conform to WCAG 2.1 AA.

The four problems we’ll fix

Each card below isolates one common structural defect. For every issue you get a plain-language statement of the problem, a Bad example (shown as escaped, non-running code so it can’t affect this page), a Good example, the copyable Code, and an ordered fix.

Fake headings and skipped heading levels

WCAG 2.2 · 1.3.1 A 2.4.6 AA EN 301 549

Screen reader users navigate by pulling up a list of every heading on the page and jumping between them — it’s the single most-used way to skim. A <div> or <p> made big and bold with CSS looks like a heading but carries no heading role, so it never appears in that list: the section is effectively invisible to anyone navigating by structure. The opposite failure is just as confusing — jumping from an <h1> straight to an <h3> implies a missing level, so a listener can’t tell whether the <h3> is a subsection of something or its own topic. Real headings, nested in order with exactly one <h1>, give the page a true outline.

Bad

The first “heading” is a styled <div> with no role, so it’s missing from the heading list. The real headings then skip from level 1 to level 3, breaking the outline.

bad-fake-headings.html
<div class="title">Annual report</div>
<p class="section-head"><strong>Revenue</strong></p>

<h1>Annual report</h1>
<h3>Revenue</h3>        <!-- skips h2 -->

Good

Every heading is a real <h1><h6> element. There is exactly one <h1>, and levels descend by one with nothing skipped, so the outline is sound.

good-headings.html
<h1>Annual report</h1>
  <h2>Revenue</h2>
    <h3>By region</h3>
    <h3>By product</h3>
  <h2>Costs</h2>

Code

When the design demands a heading-sized look that isn’t a heading (a strapline under the title), keep it a paragraph and size it with CSS. If a custom component truly can’t use an <hN> element, give it role="heading" and an explicit aria-level — but a native element is always better.

heading-role.html
<!-- A subtitle is not a heading: keep it a paragraph -->
<h1>Annual report</h1>
<p class="subtitle">Fiscal year 2025</p>

<!-- Last resort for a custom widget -->
<div role="heading" aria-level="2">Revenue</div>

How to fix

  1. Use real <h1><h6> elements for every heading; never style a <div> or <p> to fake one.
  2. Give the page exactly one <h1> that names the page’s main topic.
  3. Descend one level at a time — an <h2> may contain <h3>s, but never jump <h1> to <h3>. You may go back up any number of levels.
  4. Choose the level for meaning, not size; restyle with CSS if a level looks too big or too small.
  5. Check the result with a headings-list tool or the accessibility tree — the outline should read like a table of contents.

No landmarks (div soup)

WCAG 2.2 · 1.3.1 A 2.4.1 A EN 301 549 Section 508

Landmarks are the regions a screen reader user jumps between — banner, navigation, main, complementary, content info. They’re how someone skips the header and menu that repeat on every page and lands straight on the content, satisfying “bypass blocks”. A page built entirely from <div>s has no landmarks at all, so there is nothing to jump to: the only way through is to read or tab past every repeated block, every time. The fix costs nothing — swapping the generic wrappers for <header>, <nav>, <main>, <aside>, and <footer> creates the regions automatically. When a landmark type appears more than once (two <nav>s, say), give each an aria-label so they’re told apart.

Bad

Everything is a <div>. The accessibility tree exposes no regions, so there’s nothing to navigate by and no way to bypass the repeated header and nav (2.4.1).

bad-div-soup.html
<div class="header">…</div>
<div class="nav">…</div>
<div class="content">…</div>
<div class="sidebar">…</div>
<div class="footer">…</div>

Good

Native sectioning elements create the landmarks. <main> marks the unique content; the two navigations each get an aria-label so a user choosing between landmarks can tell them apart.

good-landmarks.html
<header>…site banner…</header>
<nav aria-label="Primary">…</nav>
<main>
  <h1>Page title</h1>
  …unique content…
  <nav aria-label="Related pages">…</nav>
</main>
<aside aria-label="Further reading">…</aside>
<footer>…</footer>

Code

Pair landmarks with a skip link so keyboard users can reach <main> in one keystroke. Add an id and tabindex="-1" to the main element so the link can move focus into it.

skip-link-and-main.html
<a class="skip-link" href="#main">Skip to main content</a>
…
<main id="main" tabindex="-1">
  <h1>Page title</h1>
</main>

How to fix

  1. Replace the top-level layout <div>s with <header>, <nav>, <main>, <aside>, and <footer>.
  2. Use exactly one <main> per page, and don’t nest it inside another landmark.
  3. When the same landmark type repeats — multiple <nav>s or <aside>s — give each a distinct aria-label or aria-labelledby.
  4. Keep <div> for purely visual grouping with no semantic meaning; it’s the right tool when no landmark fits.
  5. Add a skip link to #main so keyboard users can bypass the repeated blocks (2.4.1).

A visual list not marked up as a list

WCAG 2.2 · 1.3.1 A EN 301 549 Section 508

When a screen reader reaches a real list it announces “list, 5 items” and, on each line, “item 2 of 5”. That count is genuinely useful: the user knows how much is coming and can decide whether to read on or skip the whole block with one keystroke. A set of <div>s or <br>-separated lines styled to look like a list conveys none of that — it’s announced as a continuous run of text with no boundaries and no count. Use <ul> when order doesn’t matter, <ol> when it does (steps, rankings), and a description list <dl> for term-and-definition pairs.

Bad

These items look like a bulleted list but are separate <div>s with a CSS bullet. There’s no list role and no item count, so the grouping is lost to assistive technology (1.3.1).

bad-fake-list.html
<div class="list">
  <div>• Wash hands</div>
  <div>• Rinse produce</div>
  <div>• Chop vegetables</div>
</div>

<!-- or just line breaks -->
Wash hands<br>Rinse produce<br>Chop vegetables

Good

A real <ul> of <li>s is announced as a list with its item count. Use an <ol> instead whenever the order is part of the meaning, such as numbered steps.

good-list.html
<ul>
  <li>Wash hands</li>
  <li>Rinse produce</li>
  <li>Chop vegetables</li>
</ul>

<ol>            <!-- when order matters -->
  <li>Preheat the oven</li>
  <li>Mix the batter</li>
</ol>

Code

For term-and-definition pairs — a glossary, metadata, key/value facts — use a description list. If a design tool forces you to remove list bullets with list-style:none, the list role can disappear in Safari/VoiceOver; restore it with role="list".

description-list.html
<dl>
  <dt>Author</dt>
  <dd>A. Turing</dd>
  <dt>Published</dt>
  <dd>1950</dd>
</dl>

<!-- Keep the role if bullets are removed -->
<ul role="list" style="list-style:none">…</ul>

How to fix

  1. If items read as a set, mark them up as a list — never fake one with <div>s, bullet characters, or <br>.
  2. Use <ul> when order is irrelevant and <ol> when sequence carries meaning (steps, rankings).
  3. Use a <dl> with <dt>/<dd> for term-and-definition or name/value pairs.
  4. Put only <li> (or <dt>/<dd>) as direct children of the list element — wrap nothing else around them.
  5. If you remove bullets with CSS and the list role drops in Safari/VoiceOver, add role="list" back.

DOM order doesn’t match the visual reading order

WCAG 2.2 · 1.3.2 A 2.4.3 A EN 301 549

Screen readers, braille displays, and the Tab key all follow the order of the HTML source, not the order things appear on screen. CSS tools like flexbox order, grid placement, and absolute positioning let you paint content in any visual sequence you like while leaving the source untouched — so the page can read correctly to the eye but completely out of order to assistive tech. A caption can be heard before the figure it describes; a “Step 2” can come before “Step 1”; focus can jump backwards up the page. The rule is simple: write the source in the order it should be read, then use CSS only for presentation, never to repair a sequence the markup got wrong.

Bad

The source lists the steps out of sequence, then uses order to paint them in the right visual order. Screen reader and keyboard users get the source order — Step 2 before Step 1 (1.3.2, 2.4.3).

bad-css-order.html
<ol class="steps">
  <li style="order: 2">Step 2: Mix</li>
  <li style="order: 1">Step 1: Measure</li>
</ol>

.steps { display: flex; }
/* Looks right on screen, reads "Step 2" first */

Good

The source is in the meaningful order, so the spoken order, the Tab order, and the visual order all agree. Flexbox is still used for layout — it just isn’t asked to reorder content.

good-source-order.html
<ol class="steps">
  <li>Step 1: Measure</li>
  <li>Step 2: Mix</li>
</ol>

.steps { display: flex; }
/* No `order` overrides; source = reading order */

Code

The same trap hits absolute positioning: pulling a caption visually above a figure while it sits after it in the source. Keep related content together and in order in the markup — here the <figcaption> reads with its image regardless of where CSS paints it.

figure-source-order.html
<figure>
  <img src="chart.png" alt="Sales up 12% in Q3">
  <figcaption>Quarterly sales, 2025</figcaption>
</figure>
<!-- Style the caption's position with CSS,
     but keep it after the image in the source. -->

How to fix

  1. Write the HTML in the order the content should be read and tabbed through.
  2. Use CSS flexbox and grid for layout, but avoid order and grid-row/grid-column placement that moves content away from its source position when sequence matters.
  3. Don’t use absolute positioning to relocate meaningful content above earlier source content.
  4. Never set a positive tabindex to patch focus order — fix the source instead.
  5. Test by tabbing through the page and by reading it with a screen reader or with CSS disabled; the order should still make sense (1.3.2, 2.4.3).

Recap

  • Build the outline from real <h1><h6> elements, one <h1> per page, with no skipped levels. A bold <div> is not a heading (1.3.1, 2.4.6).
  • Wrap the page in landmark elements — <header>, <nav>, <main>, <aside>, <footer> — and label repeated landmarks so users can bypass blocks (1.3.1, 2.4.1).
  • Mark every visual list up as a real <ul>, <ol>, or <dl> so the item count and structure are announced (1.3.1).
  • Keep the DOM order equal to the meaningful reading order; never use CSS order, positioning, or grid placement to fix a sequence that the source got wrong (1.3.2, 2.4.3).

The same semantic markup satisfies WCAG, EN 301 549, Section 508, and ADA Title II at once — choose the element that matches the meaning and you meet them all.