h4 was the right size
The browser hands you six sizes and calls them levels, which is most of why this goes wrong. The elegant fix from 2013 was never implemented by anything, anywhere.
And it was! It came out at the size the design asked for, the person writing it moved on to the next section. I would put money on having done exactly the same thing myself on a site I was pleased with at the time.
Here’s a page I audited last month as a list of its headings, which is how a decent number of people meet a page:
The company name
What we do
How it works
Fees
Four entries, and the subject of the page is not one of them. The markup, roughly:
<h1><img src="logo.svg" alt="…"></h1>
…
<div class="page-title">Commercial property inspections</div>
…
<h4>What we do</h4>
<h4>How it works</h4>
<h6>Fees</h6>
Nobody did anything crazy here. The h1 is the logo because the logo is the biggest thing on the screen. The sections are h4 because h4 came out right. The h6 is a smaller subheading, visually. And the page title is a div because a div takes a class, carries no default styling and gets out of your way, which is the same instinct I was writing about 10 years ago and is genuinely convenient right up until it isn’t. Four sensible decisions, all made by looking at the page, and the document they produced is nonsense.
Somebody is reading this as a list
The heading structure of a page is its contents page, and a lot of people navigate by it directly instead of reading down. Jump to the next heading, jump to the previous one, or pull up every heading on the page at once and pick from the list. Every survey of screenreader users I’ve seen puts headings at or near the top of how people find things on a page they’ve never seen before.
So the levels aren’t a description of the page filed somewhere for a machine’s benefit. They’re the navigation, and if they’re wrong the navigation is wrong. It is wrong in a way that nothing on the screen will tell you about.
Picking those levels by size is shelving books by how tall they are. It looks tidy from across the room and it has quietly destroyed the only thing the shelf was for.
The browser handed you six sizes and called them levels
This is the part I think does most of the damage. It isn’t ignorance.
h1 { font-size: 2em; }
h2 { font-size: 1.5em; }
h3 { font-size: 1.17em; }
h4 { font-size: 1em; }
h5 { font-size: 0.83em; }
h6 { font-size: 0.67em; }
Six tags arrive pre-styled at six descending sizes. Which means the only feedback the element gives you at the moment you type it is how big the text got, so the choice gets made with your eyes, every time, by everyone, including me. Look at h6 for a second though. That’s 0.67em, which is smaller than the paragraph underneath it. The browser will happily set a heading at two thirds the size of the body copy it introduces. I think that tells you how much thought the sizing half of this ever got.
The rule underneath all of it is that level expresses nesting and nothing else. A subsection of a subsection is an h3 whether it’s set at 24px or 14px.
The outline algorithm that never was
Worth flagging, because there’s a decade of guidance out there saying the opposite of everything above.
When HTML5 arrived it came with a proposed document outline algorithm, where sectioning elements created implicit levels. You’d write an <h1> inside every <section>, nest the sections, and the real outline would be computed from the nesting instead of from the numbers. It’s an elegant idea, it makes components genuinely portable, and I wrote pages that way for a while.
No browser ever implemented it. No assistive technology ever implemented it. It sat in the spec for years as a thing that was supposed to be true. It eventually came out and was replaced with advice to use the numbered levels that match the actual structure.
The one piece of it that did get implemented, and this is the part that gets me, is the font size. Browsers carried a default rule that shrank a nested <h1> so it looked like a lower-level heading, so the presentational half of an idea about structure arrived and the structural half never did. They’ve been walking that rule back too.
Which leaves a lot of pages. There are a lot of them from roughly 2013 to 2018, whose outline is completely flat. Every heading a level one, all apparently equal, no hierarchy at all. That’s worse than the too-many-h4s version, because at least the h4 page looks wrong when you go and read it. This one looks correct in the source and is unusable in the list.
The rules, which are short
One h1 per page and it’s what the page is about. Not the site name, which is identical on every page and therefore says nothing about this one. The logo is an image inside a link home, it does not need a heading wrapped around it, and taking that heading away has never once changed how the page looks.
Don’t skip levels going down. h2 to h4 implies a level that isn’t there, so somebody hears a gap and wonders what they missed. Going back up is fine and means nothing sinister. An h4 followed by an h2 just says that subsection is over.
If it looks like a heading and isn’t one, don’t mark it as one, and the reverse. A bold line introducing a paragraph is usually not a heading. A quote set large is definitely not.
And if you find yourself wanting an h7, the answer is not role="heading" aria-level="7", which is legal and which I have never needed once after going back and fixing the nesting that got me there.
Two systems, six tags
The real problem sitting under all of this is a design problem and not a markup one, which is why telling people the rules doesn’t fix it.
A designer produces a visual hierarchy. It has 3 or 4 distinguishable sizes in it, because past 4 nobody can tell them apart and it reads as noise. A document has as many levels of nesting as it happens to have, which on a long page is 5. Those two numbers were never going to map onto each other, and trying to do both jobs with the same six tags is exactly why everyone ends up choosing tags by appearance.
So treat them as two systems. The level comes from the structure, the size comes from a class.
/* structure picks the tag, appearance picks 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; }
<h1 class="t-xl">Commercial property inspections</h1>
<h2 class="t-lg">What we do</h2>
<h3 class="t-md">Building inspections</h3>
<!-- deeper than the one above it, same size, on purpose -->
<h4 class="t-md">For landlords</h4>
That last pair is the whole point. Level 4 and level 3 look identical because visually they are both “a subheading,” and structurally one of them lives inside the other, and both of those facts are now sayable at the same time. It’s the design system argument I made in 2017 wearing a different hat. Build the scale once, as a scale, and stop letting element defaults make decisions on your behalf.
The dropdown says Heading 1
And now the recurring one, which I have never fully solved.
The client’s CMS has a dropdown offering Heading 1, Heading 2, Heading 3. The person using it picks by looking at what happens, because what happens is the only feedback the interface gives them, which is the same trap as the browser defaults with a friendlier label on it. A few years later the blog is full of h3s used as emphasis and the occasional h1 used for a big statement, and none of it was carelessness. It was the interface working as designed. It’s what a CMS does to every decision you leave lying around inside it. Many such cases!
It’s partly worked for me to (1) constrain the editor, which means taking h1 out of the options entirely since the template supplies it, and taking out any level that isn’t structurally available in that context, and (2) rename what’s left in the config to “Section” and “Subsection” instead of by number. That second one sounds like nothing and it changes how people choose, which I did not expect the first time I tried it.
Then explain it once, in one sentence, to whoever actually writes the pages: these aren’t sizes, they’re the contents page, and somebody out there uses it to get around. That’s the whole briefing, and my results with it have been partial at best and temporary at worst 🥲
Go and turn the styles off
Turn them off and read what’s left, which is the first move in the 15-minute audit and still the one I get the most out of. The page stops being a design and becomes a document, and the outline is either sensible or it very obviously isn’t. It takes about 30 seconds and no tooling.
The browser’s accessibility tree panel will show you the same thing with the styles left on. There are extensions that list every heading in order with its level, which is closer to what people actually experience.
Nothing looked different
I fixed that page in about 40 minutes. The h1 became the page title, the logo went back to being a link with alt text on the image, the two sections became h2s, and Fees became an h3 underneath the section it belongs to. Then I took a before and after screenshot to send over and they were the same screenshot, because the classes were carrying the sizes the whole time.
Which is the honest difficulty with this kind of work, and not a triumphant ending. The list that nobody with a mouse ever opens went from useless to correct, the page went from wrong to right in the only sense that matters here. There was nothing to show anybody for it. I still think it’s 40 minutes better spent than most of what I did that week.