( Operable / WCAG 2.4.6 )
Heading is empty or contains only whitespace
What is this issue?
A heading element is present in the DOM: the tree structure the browser builds from your HTML, representing every element and how they nest, which assistive technology reads instead of the visual layout, but its trimmed text content and any aria-label are both empty. The element still carries a heading role; it just has nothing for that role to announce.
This is different from a heading that has text a sighted user would consider unhelpful, like “More”; that heading has content, just weak content, which is a separate issue. An empty heading has no content at all: no visible words, no icon with an accessible name, nothing an assistive technology user can hear or read.
Why does this matter?
Screen reader users jump between sections using a heading-navigation shortcut, moving from one heading to the next without reading everything in between. When that shortcut lands on an empty heading, the screen reader announces a level, “heading level 2,” and then falls silent, because there’s no text to read next.
That silence isn’t neutral. The user has to decide whether something failed to load, whether they missed content, or whether the heading is simply broken, and the only way to find out is to keep navigating past it and hope the next stop makes more sense. On a page with several empty headings (often left behind by a template that renders a heading tag around dynamic content that sometimes comes back blank), every one of those stops adds a moment of doubt to what should be a fast, confident scan of the page.
Who is affected?
- Screen reader users: land on a heading-level announcement with no text to follow it, and can’t tell whether the section is genuinely empty or the heading itself is broken.
- Cognitive disabilities: users who rely on headings as anchors to orient themselves in a long page lose that anchor at exactly the point they expected one, adding confusion to a task that headings are supposed to simplify.
What users experience
Nadia uses NVDA on her Windows laptop to skim a product support article, pressing H to jump from heading to heading looking for the “Returns” section. NVDA announces “heading level 3” between two sections, then goes quiet: no words follow it. She presses H again to check if she missed something, backs up once, and re-reads the surrounding paragraphs to work out what that empty stop was supposed to introduce, losing the thread of the fast scan she was doing a moment earlier.
How do I fix it?
Add meaningful text inside the heading that describes the section it introduces. This works because a heading’s only job is to be announced as a navigation point with a name attached; once real text is present, the “heading level 2” announcement is followed by something a user can act on immediately.
If the heading was never meant to hold content (for example, an empty tag left behind by a component that renders a heading wrapper regardless of whether a title was supplied), remove the element and recreate any visual spacing with CSS on a non-heading element instead. Don’t leave an empty heading in place “just in case” content arrives later; render the heading tag conditionally, only when there’s real text to put inside it.
Code Examples
<h2></h2>
<h3 class="section-spacer"></h3><!-- Method 1: give the heading real text -->
<h2>Customer reviews</h2>
<!-- Method 2: if it was only ever there for spacing, drop the heading tag -->
<div class="section-spacer"></div>The first heading gets the text it was missing, so the level announcement is immediately followed by a real section name. The second element was never introducing content; swapping it for a plain <div> removes it from the heading-navigation shortcut entirely, since a <div> carries no heading role for a screen reader to stop on.
Common Mistakes
Mistake: “The heading has a CSS ::before icon, so it isn’t really empty.” Content inserted with ::before or ::after is decorative CSS output; it isn’t part of the accessible DOM (the tree structure the browser builds from your HTML) text content, so a screen reader ignores it completely. A heading that looks like it has an icon in the browser can still be entirely empty to assistive technology.
Mistake: “An empty heading with aria-hidden="true" is fine because screen readers skip it.” aria-hidden="true" does stop a screen reader from announcing the element, so technically it’s no longer an empty-heading failure in the strict sense, but it also removes a legitimate heading from the page’s structure for every assistive technology user. If the heading was meant to organize content, hiding it is worse than fixing it; only use aria-hidden on a heading you’ve deliberately decided shouldn’t exist in the structure at all.
Mistake: “This only happens on broken pages, not production code.” Empty headings are most common in components that render a heading tag unconditionally around a CMS field or API response; the tag exists in the template even on the days the content is missing. The fix belongs in the component logic (only render the heading when there’s text to put in it), not in a one-off content edit.
How RedFlag Detects This
Custom automated: RedFlag’s own DOM detector, runs on every scan. RedFlag walks every <h1> through <h6> element on the page and flags any whose trimmed text content and aria-label are both empty. This check reuses the same rule id as a native axe-core check but runs as RedFlag’s own custom detector so it can control its own severity and messaging.
False negative: the check only inspects textContent and aria-label; a heading whose accessible name comes from aria-labelledby pointing at visible text elsewhere on the page has empty textContent and no aria-label, yet genuinely has a name, so a mislabeled edge case here could theoretically go the other way and get flagged when it shouldn’t. Separately, a heading holding only a zero-width space character isn’t stripped by a plain trim, so that heading can pass as “non-empty” while still being silent to a screen reader. False positive: none typical beyond the aria-labelledby case above, since checking for genuinely empty text is otherwise binary. Manual step: for any flagged heading, confirm with a screen reader that it truly announces nothing, and separately check any heading using aria-labelledby to confirm its resolved name is real and accurate.
Manual Testing
- Open the page in Chrome or Firefox with NVDA or JAWS running on Windows, or VoiceOver on macOS.
- Use the heading-navigation shortcut (NVDA/JAWS: H to move forward through headings; VoiceOver: Control+Option+Command+H) to move through every heading on the page in order.
- Listen for a heading level announcement followed immediately by real words, for example “heading level 2, Customer reviews.”
- If you hear a level announced with a pause and nothing after it, that heading fails.
- Cross-check visually: an element that renders no visible text but still occupies a heading tag in the DOM (inspect it in browser dev tools) confirms the failure even before you test with a screen reader.
Related WCAG Success Criteria
2.4.6 Headings and Labels: Headings and labels must describe their topic or purpose. An empty heading fails this at the most basic level: there’s no topic described because there’s no text at all, which is a stricter failure than a heading whose text is merely vague.
Related Issues
Heading doesn’t describe its section is the next failure mode on the same spectrum: a heading that has text, but text too generic to tell a screen reader user what the section actually contains.
Heading levels are skipped covers the other way heading navigation breaks: the headings all have real text, but the level sequence jumps and misrepresents how sections relate to each other.
Element has ambiguous or unclear accessible name and Element label lacks sufficient context cover the same “technically named, practically useless” problem on interactive elements instead of headings: the same underlying lesson that a name must actually communicate something, not just exist.
References
- W3C Understanding 2.4.6: Headings and Labels
- MDN: Heading elements
- WebAIM: Semantic Structure - Headings
Frequently asked questions
Does a heading with only an icon inside it count as empty?
It depends on the icon. An inline SVG or icon font with no accessible name of its own leaves the heading with empty text content, so it still fails. Give the icon an accessible name with aria-label, or add visible text alongside it inside the heading.
Can I use an empty heading for spacing between sections?
No. A heading exists to be announced as a navigation point, not to add visual space. Use CSS margin or padding on a non-heading element instead, so screen reader users are not interrupted by a heading that announces nothing.
Does axe-core catch this the same way RedFlag does?
Yes, axe-core also ships an empty-heading rule with the same core logic. RedFlag runs its own copy of this check as a custom detector rather than calling axe-core for it, but the underlying condition it looks for is the same: a heading with no accessible text.
Is a heading that only contains a non-breaking space still empty?
Visually and programmatically, yes. A non-breaking space renders as blank space, and RedFlag trims whitespace before checking a heading text content, so a heading holding only spacing characters is still flagged as empty.
What should I hear when I land on a correctly filled heading?
A screen reader should announce the heading level followed immediately by real words, for example "heading level 2, Customer reviews." If you hear only the level with a pause and nothing else, the heading is empty.