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.1A2.4.6AAEN 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.
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
Use real <h1>–<h6> elements for every
heading; never style a <div> or <p> to
fake one.
Give the page exactly one <h1> that names the page’s main
topic.
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.
Choose the level for meaning, not size; restyle with CSS if a level looks
too big or too small.
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.1A2.4.1AEN 301 549Section 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).
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.
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
Replace the top-level layout <div>s with
<header>, <nav>,
<main>, <aside>, and
<footer>.
Use exactly one <main> per page, and don’t nest it inside
another landmark.
When the same landmark type repeats — multiple <nav>s or
<aside>s — give each a distinct aria-label or
aria-labelledby.
Keep <div> for purely visual grouping with no semantic
meaning; it’s the right tool when no landmark fits.
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.1AEN 301 549Section 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
If items read as a set, mark them up as a list — never fake one with
<div>s, bullet characters, or <br>.
Use <ul> when order is irrelevant and
<ol> when sequence carries meaning (steps, rankings).
Use a <dl> with <dt>/<dd>
for term-and-definition or name/value pairs.
Put only <li> (or <dt>/<dd>)
as direct children of the list element — wrap nothing else around them.
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.2A2.4.3AEN 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.
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
Write the HTML in the order the content should be read and tabbed through.
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.
Don’t use absolute positioning to relocate meaningful content above earlier
source content.
Never set a positive tabindex to patch focus order — fix the
source instead.
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.