← All writing
Craft · · 6 min

Heading levels are an outline

On the table of contents a lot of people navigate by, the advice from 2013 that turned out to be wrong, and the two systems everybody is trying to do with one.

HTML Accessibility

The markup, roughly:

what I found
<h1><img src="logo.svg" alt="Marsden & Clough"></h1><div class="page-title">Commercial property surveys</div><h4>What we do</h4>
<h4>How it works</h4>
<h6>Fees</h6>
HTML

Nobody did anything mad here. The h1 is the logo because the logo is the biggest thing. The sections are h4 because h4 came out the right size. The h6 is a smaller subheading, visually. Every one of those decisions was made by looking at the page and it produced a document whose outline is nonsense.

The outline is a navigation control

Here’s the framing that makes all of this click, and it’s not really about semantics.

The heading structure of a page is a table of contents, and a substantial number of people navigate by it directly. Screen reader users can jump from heading to heading, or pull up a list of all the headings on the page and pick one. Every survey of screen reader users I’ve seen has headings near the top of the list of how people find things on an unfamiliar page.

Which means the heading markup isn’t a description of the page for a machine’s benefit. It’s a control surface. It’s the equivalent of a menu, and if the levels are wrong the menu is wrong.

On the page above, somebody listing the headings gets: the company name, then four things at level four with nothing above them, and the actual subject of the page isn’t in the list at all because it’s a div.

The heading structure isn't a description of the page for a machine's benefit. It's a navigation control, and a lot of people use it as one.

The rules, which are short

One <h1> per page, and it’s what the page is about. Not the site name, which is the same on every page and therefore carries no information about this one. The logo is an image and a link home, and it doesn’t need a heading around it.

Don’t skip levels downwards. h2 to h4 implies a missing level, so a listener hears a gap and wonders what they missed. Going back up is fine: h4 followed by h2 simply means that subsection has ended.

Level expresses nesting, not size. A subsection of a subsection is h3 whether it’s set in 24 pixels or 14.

And if it looks like a heading and isn’t one, don’t mark it as one, and vice versa. A bold line introducing a paragraph is not necessarily a heading. A pull quote set large is definitely not.

What a lot of old advice got wrong

Worth flagging, because there’s a decade of guidance out there saying the opposite.

When HTML5 arrived there was a proposed document outline algorithm, where sectioning elements would create implicit levels, so you could use <h1> inside every <section> and the outline would be computed from the nesting. It’s an elegant idea and I wrote pages that way.

No browser ever implemented it. No assistive technology ever implemented it. It sat in the specification for years as a thing that was supposed to be true, and it was eventually removed and replaced with advice to use the numbered levels that reflect the actual structure.

So if you have pages built on that assumption, and there are a lot of them from around 2013 to 2018, their outline is flat: every heading is a level one, all apparently equal, with no hierarchy at all. Which is worse than the too-many-h4s version, because it looks correct in the source.

Two systems, not one

The real problem underneath all of this, and it’s a design problem rather than a markup one.

A designer produces a visual hierarchy, and it usually has three or four distinguishable sizes in it, because more than four is visual noise and nobody can tell them apart.

A document has as many levels of nesting as it has, which on a long page can be five.

Those two don’t map, and they were never going to, and trying to do both jobs with the same six tags is why everybody ends up choosing tags by appearance.

The answer is to treat them as two independent systems. The level comes from the structure. The size comes from a class.

headings.css
/* structure decides the tag, appearance decides the class */
.t-xl { font-size: 2.25rem; }
.t-lg { font-size: 1.5rem; }
.t-md { font-size: 1.25rem; }
.t-sm { font-size: 1rem; font-weight: 600; }
CSS
page.html
<h1 class="t-xl">Commercial property surveys</h1>
<h2 class="t-lg">What we do</h2>
<h3 class="t-md">Building surveys</h3>
<h4 class="t-md">For landlords</h4>   <!-- deeper, same size, deliberately -->
HTML

That last line is the whole point. Level four and level three look identical, because visually they’re both “a subheading,” and structurally one is inside the other. Both facts are now expressible.

This is the design system argument from 2017 wearing different clothes: build the type scale once, as a scale, and stop letting element defaults make decisions for you.

The CMS dropdown

And the recurring problem, which I have never fully solved.

The client’s editor has a dropdown that says Heading 1, Heading 2, Heading 3, and the person using it picks by looking at the result, because that is the only feedback the interface gives them. So over four years the blog fills with h3s used as emphasis and h1s used for the occasional big statement.

What’s actually worked, partially: constrain the editor. Remove h1 from the options entirely, since the page template supplies it. Remove any level that isn’t structurally meaningful in that context. Rename the options in the configuration to “Section” and “Subsection” rather than by number, which sounds like nothing and changes how people choose.

And then explain it once, in one sentence, to the person who actually writes: these aren’t sizes, they’re the contents page, and somebody uses it to get around.

How to check, in 30 seconds

Turn the styles off, which is the first move in the 15-minute audit I wrote up in 2019, and read what’s left. The page becomes a document and the outline is either sensible or it isn’t, and you can tell immediately.

The browser’s accessibility tree panel will show it too, and there are extensions that list headings in order with their levels.

Identical, either way

Fixed that page in about 20 minutes. The h1 became the page title, the logo became a link with alt text, the sections became h2, and the fees section became an h3 under the one it belongs to. Nothing looked different afterward, because the classes carried the sizes.

Which is the frustrating and useful thing about most of this work: the correct version and the broken version are visually identical, so nobody notices either the problem or the fix.

Read similar posts
6 min

details and summary

An FAQ page arrived on my desk with about 200 lines of JavaScript running an accordion, and the two elements that do it natively have been in every browser since 2020 and got the missing feature last winter.

6 min

dialog

I have built the same modal four times across four projects, each one slightly different, each one getting a different two of the six behaviors wrong, and there has been an element for it since before the second one.