Headings, landmarks and structure
Module 1 taught you which structural elements exist. This lesson is about what a screen reader user actually does with them, because once you have watched that, the reason for the rules stops being abstract.
Nobody reads a page top to bottom
A sighted visitor scans — headings, bold text, images — and jumps to the part they want. That takes about two seconds and needs no thought.
A screen reader user does the same thing, with different tools. They do not listen from the top. They pull up a list.
In VoiceOver, Ctrl + Option + U opens the rotor: a menu of the page's headings, or its
links, or its landmarks, or its form controls. In NVDA, Insert + F7 lists elements the
same way. H jumps to the next heading, D to the next landmark, K to the next link,
1 to the next level-1 heading.
Your headings and landmarks are that menu. They are not typography — they are the table of contents that decides whether the page can be navigated at all.
Which reframes everything:
- A page with no headings has an empty menu. The only option is to listen to everything, in order.
- A page whose headings are chosen for size has a menu in the wrong order.
- A
<div class="title">styled to look like a heading is not in the menu at all.
Heading structure as an outline
<h1>Sharma Kirana</h1>
<h2>Our prices</h2>
<h3>Atta and rice</h3>
<h3>Oil and ghee</h3>
<h2>Delivery</h2>
<h3>Areas we cover</h3>
<h2>Contact</h2>
Read the headings alone and you should have a fair summary of the page. That is the test, and it is the same test as a document's table of contents.
The rules, and the reason for each:
One <h1>, saying what this page is. Not the site name repeated on every page —
About Sharma Kirana, Delivery areas. A screen reader user pressing 1 expects to
land on the page's subject.
Never skip a level going down. h1 then h3 makes a listener wonder what they
missed. Going back up is fine — h3 then h2 starts a new section.
Choose the level for structure, never for size. If your h2 is too big, that is one
line of CSS. This was the module 1 rule and it is worth repeating because the temptation
is constant.
Do not use a heading for something that is not one. A short bold line introducing a
paragraph is a <p> with a class, or a <strong>. Making it an h4 puts noise in the
menu.
Checking it
Devtools does this for you. In Chrome, Elements → Accessibility → and the full tree; or run this in the console:
[...document.querySelectorAll("h1,h2,h3,h4,h5,h6")]
.forEach(h => console.log(" ".repeat(+h.tagName[1] - 1) + h.tagName + " " + h.textContent.trim().slice(0, 60)));
That prints your outline, indented. If it does not read like a sensible contents page, neither does the page. It also makes a skipped level obvious, because the indentation jumps two steps.
Landmarks
The second menu. Each of these creates a region a user can jump straight to:
<body>
<header> <!-- banner -->
<nav>…</nav> <!-- navigation -->
</header>
<main>…</main> <!-- main -->
<aside>…</aside> <!-- complementary -->
<footer>…</footer> <!-- contentinfo -->
</body>
<main> is the one to get right. It is what "skip to main content" targets, and
without it a keyboard user tabs through your entire navigation on every page. One per
page, not nested inside the others.
Two details worth knowing:
<header> and <footer> are only landmarks at page level. Inside an <article> or
<section> they are just grouping elements, which is correct and means you can use them
freely inside components.
Label your landmarks when there is more than one of a kind. Two <nav>s produce two
identical "navigation" entries in the menu:
<nav aria-label="Main">…</nav>
<nav aria-label="Breadcrumb">…</nav>
<nav aria-label="Footer">…</nav>
Do not write aria-label="Main navigation" — the role is already announced, so the
listener hears "Main navigation navigation". The label is the distinguishing word only.
You can also point at a visible heading instead, which is better when one exists:
<section aria-labelledby="delivery-heading">
<h2 id="delivery-heading">Delivery</h2>
…
</section>
aria-labelledby reuses text that is already on screen, so it cannot drift out of sync
and it is translated along with the page. Prefer it to aria-label whenever there is
visible text to point at.
Lists and tables are structure too
From module 1, with the reason attached now:
A <ul> is announced as "list, 5 items", and each item as "item 2 of 5". That count
is orientation — it tells a listener how much is coming and where they are. A stack of
<div>s gives none of it, and there is no way to recover it.
A <table> with scope lets a user land on a cell and hear "Karve Nagar, Charge,
₹20". Without it they hear "₹20" and have to remember which column they are in while
navigating a grid by keyboard. That is the difference between a usable table and an
unusable one, and it is two attributes.
Reading order is DOM order
The single most important consequence of modules 3 and 4.
A screen reader and the keyboard follow the DOM, not your CSS. So order in
Flexbox, grid-template-areas, grid-auto-flow: dense and position: absolute can all
put the visual order and the reading order out of step — and when they do, a keyboard
user tabs around the screen in an order that makes no sense while a listener hears the
page in an order that does not match what anybody sees.
Get the HTML order right and you never have this problem. The module 3
practice-layouts skeleton is main before aside for exactly this reason: the mobile
order is the DOM order, so nothing needs rearranging and content comes before the
sidebar at every width.
The test: tab through the page and watch where the focus ring goes. If it jumps around, fix the HTML, not the CSS.
Language
<html lang="en">
…
<p>The shop is called <span lang="hi">किराना</span>.</p>
From module 1, with the consequence stated: a screen reader picks its voice and
pronunciation rules from lang. English read by a Hindi voice is not slightly wrong,
it is unintelligible — and the reverse is equally true. On a page mixing English and
Devanagari, marking the spans matters.
Page titles
<title>Delivery areas — Sharma Kirana</title>
The <title> is the first thing a screen reader announces on page load, and it is
how somebody with twelve tabs open tells them apart. Specific part first, as module 1
said. Every page needs its own.
Check your work
How a screen reader user navigates. By pulling up a list of headings, landmarks or links — not by listening from the top.
What your headings actually are. That menu.
What a <div class="title"> is worth. Nothing — it is not in the menu.
The heading test. Read the headings alone: do they summarise the page?
Why never skip a level. A listener wonders what they missed.
The landmark to get right. <main>, because "skip to main content" targets it.
When <header> is not a landmark. Inside an <article> or <section>.
How to label two navs. aria-label with the distinguishing word only — not
"Main navigation", or it is announced twice.
Why aria-labelledby beats aria-label. It reuses visible text, so it cannot drift
and it gets translated.
What a list announcement gives you. The item count and your position in it.
What reading order follows. The DOM, not the CSS — so fix the HTML, not the layout.
What lang changes. The voice and pronunciation rules.
What is announced first on page load. The <title>.
Practice
- Run the heading-outline console snippet on your own page. Read the output aloud and judge whether it summarises the page.
- Run it on three real websites. Most have problems; name them.
- Skip a heading level deliberately and look at the indentation in the output.
- Find a
<div>styled as a heading on any site and confirm it does not appear in the outline. - Open the devtools Accessibility tree for your page and find every landmark.
- Remove
<main>and try "skip to main content" navigation in a screen reader. - Put two
<nav>elements on a page with no labels, then list landmarks in a screen reader. Addaria-labelto each and list again. - Write
aria-label="Main navigation"and listen to it. Then just"Main". - Replace an
aria-labelwitharia-labelledbypointing at a visibleh2. - Convert a stack of
<div>s to a<ul>and listen to both. - Reorder a Flexbox row visually with
order, then tab through it and watch the focus ring. - Put
<aside>before<main>in the HTML, keep the visual order withgrid-template-areas, then navigate with a screen reader. - Set
lang="hi"on an English page and listen to a sentence. - Give two pages the same
<title>and try to tell them apart from the tab strip.
Official documentation
- W3C WAI — Page structure tutorial — Headings, landmarks and labelling, written from the assistive-technology side.
- W3C WAI — Headings — Why nesting matters, with the navigation behaviour explained.
- MDN — ARIA landmark roles — Which HTML element maps to which landmark.
- W3C — Understanding WCAG 1.3.1 Info and Relationships — The criterion behind semantic structure.
- W3C — Understanding WCAG 2.4.6 Headings and Labels — The requirement that headings describe their content.
Next: the keyboard, which is the test that catches the most.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship