Search topics
Headings chosen by size, not by structure
A recipe page styles its main section titles as divs, uses an h5 for a small title and an empty h2 to draw a rule, so a screen reader's headings list misses the sections the page is for.
<div class="section-title">Method</div> The problem
Screen reader users can't skim the page by its headings, because the main sections aren't headings at all.
A recipe page for lentil soup has a large title, a short introduction and a thin rule. Below the rule are two sections, Ingredients and Method, each under a bold title. A smaller bold title, Tips, closes the method, and a row of related recipe cards ends the page under the title More. To a sighted reader the outline is plain. A screen reader user who wants the method does what most people do on a long page: they open the headings list, or press H to jump from heading to heading. The list reads "Lentil soup, heading level 1", "heading level 2", "Tips, heading level 5" and "More, heading level 2". Ingredients and Method, the two sections the page exists for, are missing. One entry has no text, and the last doesn't say more of what.
Each title was marked up for how it should look, not for what it is. 1.3.1 needs the structure people see to be in the code too: here, which lines are section titles and which section sits inside which. 2.4.6 needs headings to describe what they introduce. The page breaks that in four places:
- Ingredients and Method are
divelements with a class that makes them large and bold. They look like headings and aren't, which is WCAG failure F2. - Tips is an
h5because the design's h5 was the right size. It belongs to Method, a level 2 section, so its level puts it three levels deeper than it is. - The rule under the introduction is an empty
h2with a top border, so the headings list has a stop with no text. - The related recipes are headed More. Beside the cards that reads well enough, but in a list of headings it says nothing about what follows.
Nothing on the screen is wrong: size, weight and spacing all show the outline. The failure is in the markup, so it reaches the people who get a page's structure from the code instead of from how it looks.
2 of 10 groups affected
- No vision (affected)
- Low vision (affected)
- Colour vision (not affected)
- No hearing (not affected)
- Hard of hearing (not affected)
- No speech (not affected)
- Motor (not affected)
- Reach, strength (not affected)
- Cognitive (not affected)
- Seizures (not affected)
- Without vision Screen reader users skim a long page by its headings, from the headings list or by pressing H. Ingredients and Method aren't in either, so they have to read the page line by line to find the steps.
- Limited vision People who use a screen reader alongside magnification often move by heading rather than pan across the page, and land on an empty stop and a Tips heading with no Method above it.
axe reports the empty h2 (empty-heading) and the jump from h1 to h5 (heading-order), but classes both as best practice, so many audits leave them out. Neither rule can see a div that looks like a heading, so the two missing sections, the largest part of the failure, pass every scan. Fixing only what the scanner reports can make things worse: changing the h5 to an h2 clears heading-order and puts Tips on the same level as Method. A quick AI fix often goes the other way and turns every bold line into a heading, the serving size and cooking time included, which fills the list with stops nobody wants. Whether each line is a heading, at which level, and whether its words describe its section, are things a person has to judge against the page.
Try it
Use the headings list to jump to the method.
The list shows the headings a screen reader finds on this recipe page, and the tag beside each title shows its markup. Choose an entry to jump to it. Can you reach Method?
Broken Can you fix this?
<h1>A thick, warming soup for weeknights.
<h2><div>- 1 onion
- 2 carrots
- 200 g red lentils
- 1 litre vegetable stock
<div>- Soften the onion and carrots.
- Add the lentils and stock.
- Simmer for 25 minutes, then blend.
<h5>A squeeze of lemon brightens it.
<h2>- Tomato soup
- Pea soup
- Leek and potato soup
Screen reader’s headings list:
A recreated demo. The broken version is intentionally inaccessible.
The fix
Mark every section title as a heading at the level of its place in the outline, and style it with a class.
- 1Every visible section title is a heading: an h1–h6 element, or role="heading" with aria-level.
- 2Take the level from the outline and the size from a class.
- 3Every heading has words that name its section, and nothing else is a heading.
<h1>Lentil soup</h1><p>A thick, warming soup for weeknights.</p><h2 class="divider"></h2> <div class="section-title">Ingredients</div><ul>…</ul> <div class="section-title">Method</div><ol>…</ol><h5>Tips</h5><p>…</p> <h2>More</h2><ul class="cards">…Tomato soup, Pea soup, Leek and potato soup…</ul>Heading elements at the right level
<h1>Lentil soup</h1><p>A thick, warming soup for weeknights.</p> <h2 class="section-title divider">Ingredients</h2><ul>…</ul> <h2 class="section-title">Method</h2><ol>…</ol><h3 class="title-small">Tips</h3><p>…</p> <h2 class="section-title">More soup recipes</h2><ul class="cards">…Tomato soup, Pea soup, Leek and potato soup…</ul>Why this fix: Use native HTML first
The two main sections have no heading, the only level 2 heading before them is empty, Tips claims a depth its place doesn't have, and More doesn't say what it introduces.
Ingredients, Method and More soup recipes are level 2 headings under the page title, and Tips is a level 3 heading inside Method, so the headings list now reads like the page. The classes keep every title exactly the size it was, because the size never depended on the element. The rule is a border on the first section title, so it no longer adds an empty stop.
Both put every title in the headings list at the right level and meet 1.3.1. Use heading elements wherever you can change the markup: they do the same job with no attributes to keep in step, and every tool that reads the page treats them as headings, not only the ones that read ARIA. Keep role="heading" for a title whose element you can't change, such as one a third-party widget renders, and switch it to a heading element when you can.
Most heading mistakes come from tying a heading's level to its size. Three places usually do that.
- A design system's heading component. Give it a level and a size as separate props, for example
<Heading level={3} size="small">, so nobody has to pick the wrong level to get the right look. - A CMS editor. Editors choose "Heading 5" from the toolbar because it is small. Offer the sizes as styles and limit the levels to the ones a body of text can use, usually h2 to h4.
- Cards and other reused components. A card's title is an h3 under an h2 section and an h2 on a page without one, so take its level from where it is placed, not from the component.
Not every bold line is a heading. A heading introduces the content after it; the serving size, a price or a pull quote doesn't, so leave those as text, however large they are drawn. Removing a heading that was only there for its look is as much a part of the fix as adding the missing ones.
Use CSS for lines and space. A border or margin draws the same rule without adding anything to the headings list. Use an hr only where the rule marks a real change of topic, since screen readers announce it as a separator.
Short headings are fine. Tips is enough under Method, because its level says whose tips they are. More needed its extra words because it sits at level 2, beside Ingredients and Method, where nothing around it says more of what.
WCAG doesn't require one h1 or forbid every skipped level. Keep one h1 that names the page's content, and step down one level at a time, so the levels never claim a relationship the page doesn't show.
Fixing this with an AI coding assistant? Get this guide as Markdown
Verify the fix
4 checks, no mouse.
Test with: Screen reader
Record the OS, browser and assistive-technology versions, the build, the date, and the actual result of each step. Repeat whenever a template, a shared heading component or the CMS editor's styles change.
Limits
This is a starting pattern, not a guarantee for every application. Class names, colours and the … in the lists are placeholders. Screen readers word the headings list differently, so listen for the titles and levels rather than the exact phrasing. This guide covers whether titles are headings, their levels and their words. It doesn't cover whether long content has enough headings in the first place, or the labels on form fields. Test the complete journey with your target assistive technology.
This is a recreated teaching example, not a finding about a named client. The code is a starting pattern: test it in your own product. No completed assistive-technology test is claimed here.
References
- WCAG Understanding: info-and-relationships (opens in a new tab)
- WCAG Understanding: headings-and-labels (opens in a new tab)
- WCAG Technique H42 (opens in a new tab)
- WCAG Technique ARIA12 (opens in a new tab)
- WCAG Technique F2 (opens in a new tab)
- WAI Tutorial: page-structure/headings (opens in a new tab)
- developer.mozilla.org: Heading_Elements (opens in a new tab)
- dequeuniversity.com: heading-order (opens in a new tab)
- dequeuniversity.com: empty-heading (opens in a new tab)
Keep this fixed as your code changes.
Turn guides like this into rules your coding agents and CI follow, then have the result retested with a keyboard and screen readers.