Accessibility
Page structure: what a screen reader leaves of your website
A website has two versions.
10 min read
By Timo Wessels Published
A website has two versions. One you see: colours, images, columns, spacing. The other is the one that goes to screen readers, search engines and AI systems — and it consists of nothing but structure.
If this second version is just a row of meaningless boxes, the first version helps nobody. It then does not exist at all.
What actually reaches the screen reader
From your HTML the browser builds two things in parallel.
The first is the DOM — the complete structure of all elements, from which the visible page is rendered.
The second is the accessibility tree, a slimmed-down image of it. It contains only what assistive technologies need: roles, names, states and properties.
What goes in:
- An
<h1>becomes: role “heading”, level 1. - A
<p>becomes: text. - An
<img>becomes an element with the name from thealtattribute. - A
<button>becomes: role “button”, usable, with the state enabled or disabled.
What does not go in:
- A
<div>or<span>without further information. It gets the role “generic” — that is: no content, no meaning. - Everything hidden through CSS with
display: none.
The last point has a consequence that surprises many: CSS influences what ends up in the accessibility tree. What you hide visually also disappears for the screen reader. That is usually right — but it is the reason a navigation hidden by CSS can sometimes still be tabbed through by keyboard users when it was hidden by other means.
The practical consequence: native HTML elements create roles and states by themselves. A <button type="button">Request a quote</button> is recognisable and usable as a button for a screen reader without any extra work. A <div> with a click event that looks exactly the same is not — you cannot reach it with the keyboard, it is not announced as a button, and it does not react to Enter.
That is the core of the topic: use the right element, and eighty percent of the work never arises.
(Rheinwerk, Barrierefreie Webseiten, ch. 3.7)
Semantic HTML
Semantics means choosing an element for what the content is — not for how it should look.
Every HTML element carries a meaning. With exactly two exceptions: <div> and <span> mean nothing. They are pure containers.
The difference in practice:
<!-- without meaning -->
<div class="heading">Services</div>
<div class="text">We offer three services:</div>
<div class="list">
<div>Accessibility audit</div>
<div>Loading time analysis</div>
<div>Maintenance</div>
</div>
<!-- with meaning -->
<h2>Services</h2>
<p>We offer three services:</p>
<ul>
<li>Accessibility audit</li>
<li>Loading time analysis</li>
<li>Maintenance</li>
</ul>
Both can look visually identical. But in the second version the screen reader announces: “heading level 2, Services” and “list with three items”. The user knows what to expect and can jump straight to the list. In the first version they hear three words without connection.
The same applies to search engines and to AI systems evaluating your page: they read the structure, not the appearance.
The criterion behind it is 1.3.1 “Info and Relationships”, level A: structure and relationships that can be seen visually must also be present in the code.
(Rheinwerk, Barrierefreie Webseiten, ch. 3.2)
The regions of a page
HTML knows elements that mark whole regions of a page. In the jargon they are called landmarks — points of orientation. A screen reader can output a list of these regions and jump straight to them.
| Element | What it marks |
|---|---|
<header> |
Header area with logo, title, often the navigation |
<nav> |
Navigation area |
<main> |
The main content of the page — exactly once per page |
<article> |
Self-contained content that could stand on its own |
<section> |
A thematically connected section |
<aside> |
Secondary content, sidebar, additional information |
<footer> |
Footer area with contact, legal links, copyright |
A complete basic skeleton looks like this:
<body>
<a href="#content" class="skip-link">Skip to content</a>
<header>
<a href="/"><img src="logo.svg" alt="Home page Example Ltd"></a>
<nav aria-label="Main navigation">
<ul>
<li><a href="/services">Services</a></li>
<li><a href="/contact">Contact</a></li>
</ul>
</nav>
</header>
<main id="content">
<h1>Services</h1>
<p>…</p>
</main>
<footer>
<p>Example Ltd · <a href="/imprint">Imprint</a></p>
</footer>
</body>
A detail that is often missing: if a page has several <nav> regions — main navigation, footer navigation, breadcrumb — each needs its own label through aria-label. Otherwise the user hears “navigation” three times and does not know which is which.
<main> exists exactly once per page. It is the region the screen reader user jumps to directly when they want to skip the header.
(Rheinwerk, Barrierefreie Webseiten, ch. 3.4 and 8.1)
The skip link
Success criterion 2.4.1 “Bypass Blocks”, level A: there has to be a way to skip repeated regions.
The idea behind it: the same twenty menu items stand on every subpage. Whoever moves through the page linearly — with keyboard or screen reader — would have to go through them again on every page before the actual content begins.
The usual solution is a skip link as the very first element in the <body>:
<a href="#content" class="skip-link">Skip to content</a>
It is normally invisible and becomes visible as soon as it receives focus:
.skip-link {
position: absolute;
inset-block-start: -100%;
}
.skip-link:focus {
position: static;
/* or: inset-block-start: 0; */
}
Important here: it must not be hidden with display: none. Then it is gone for the keyboard too and does not fulfil its purpose.
Two things I see regularly in practice:
- The skip link is there, but it does not become visible on focus. Then, for a sighted person, focus seems to jump into nothing.
- The skip link points to an
idthat does not exist. Then simply nothing happens.
A correctly set <main> with an id and a skip link to it that becomes visible — that is the whole solution.
The order in the source
Success criterion 1.3.2 “Meaningful Sequence”, level A: the order in the code has to match the order in which the content is meant to be read.
Screen readers read linearly, from top to bottom. What is fourth in the source comes fourth.
The conflict arises through modern CSS. Grid layouts can arrange elements freely — an element that comes first in the code can end up visually at the bottom right. For sighted people the arrangement is then right, for everyone else it is not.
A concrete example: a product image sits visually above the heading. In the code it still has to come after the heading, because the heading announces what it is about. Otherwise the user first hears the alternative text of an image and only then what the image was actually of.
The rule for practice: write the source in the order in which you would read the content aloud. Then arrange it with CSS. Not the other way round.
That also concerns the switch between desktop and smartphone: if columns appear in a different order on the phone than on the desktop, at least one of the two orders is wrong.
(Rheinwerk, Barrierefreie Webseiten, ch. 3.5)
Two small things with a big effect
The language declaration. At the very top of the document:
<html lang="de">
Without this declaration the screen reader guesses the language — and in case of doubt reads German text with English pronunciation. The result is incomprehensible. A single attribute, and it is done.
The same applies on a small scale to individual passages in another language in the text:
<p>Das Verfahren heißt <span lang="en">progressive enhancement</span>.</p>
The page title. Every page needs its own meaningful <title>. It is the first thing a screen reader announces on loading, and the only orientation when someone switches between several tabs. Five subpages with the title “Home” are a real finding.
How to check it yourself
Look at the structure. The browser extension HeadingsMap shows the heading structure and landmarks as an outline. The Web Developer extension can mark headings and regions directly on the page. Both free, both installed in a minute.
The developer tools. In Chrome and Edge there is an “Accessibility” tab under “Elements” that shows the accessibility tree for the selected element — with role, name and states. That is the most precise answer to the question: what actually arrives here?
The landmark list in the screen reader. In NVDA a list of the regions can be called up. If it shows only “main” and nothing else, the landmarks are missing.
The Tab test for the skip link. Load the page, press Tab once. Does a skip link appear? Does it lead to the content?
The look at the source. Right-click, view page source. Search for <main, <nav, <header, <footer. If none of them occurs and there is <div class="…"> everywhere instead, you have your result.
What to do
First check what your theme brings. Most of these elements come from the theme, not from your content. A cleanly built theme sets <header>, <nav>, <main> and <footer> by itself and brings a working skip link. A poorly built one puts <div> everywhere. The difference matters more when choosing a theme than any design feature — and it is the reason that in a rebuild I would rather replace the foundation than repair a hundred individual places.
Set lang="de" if it is missing. That is the cheapest finding of all.
Give every <nav> a label.
Make sure exactly one <main> exists and the skip link points to it.
And when building content: use the formats provided in the editor — heading, list, quote — instead of rebuilding text visually. A list made of paragraphs with a hyphen in front looks like a list and is none. The effort is the same, the result is not.
Sources
- Rheinwerk, Barrierefreie Webseiten, ch. 3.2 (semantic HTML) — semantics as a description of what the content is,
<div>and<span>as the only meaningless elements, the comparison example with heading and list, the HTML5 region elements, and the warning against choosing heading levels for their size. - Rheinwerk, Barrierefreie Webseiten, ch. 3.4 (semantic HTML for accessibility) — the linear output of screen readers, the division of structural elements into navigation and content structure,
<main>as a precondition for WCAG 2.4.1, the benefit of semantic markup for search engines, and HeadingsMap as a checking tool. - Rheinwerk, Barrierefreie Webseiten, ch. 3.5 (meaningful sequence) — WCAG 1.3.2 and the example that a product image has to come after its heading in the code, even if it appears above it visually.
- Rheinwerk, Barrierefreie Webseiten, ch. 3.7 (accessibility tree) — the accessibility tree as an abstraction of the DOM with roles, names, states and properties, the mapping of
<h1>,<p>,<img>and<div>, the role “generic”, the exclusion of content hidden by CSS and the resulting effect of CSS on semantics, the accessibility API implementations UI Automation and NSAccessibility, the mapping specification HTML-AAM, and the automatic creation of roles by native HTML elements using the example of a button. - Rheinwerk, Barrierefreie Webseiten, ch. 8.1 (navigation) — WCAG 2.4.1 “Bypass Blocks”, the skip link with a code example and the note to make it visible on focus through CSS, and the list of region elements with their respective function for screen reader users.
- Atlas,
accessibility/criteria/1-3-1-info-and-relationships.mdand2-4-1-bypass-blocks.md— classification of the criteria by principle, guideline and conformance level. - Own audit practice — the two most common skip link errors (not visible on focus, target
idmissing), labelling several<nav>regions througharia-label, the quick source test for region elements, the classification of theme quality as the most important lever, and the note on lists rebuilt with hyphens in the editor.