Empty headings: what counts as empty and how to check
A heading with no text still shows up in the screen reader outline, as a blank entry. squirrel flags every one on every page.
What an empty heading is
Screen readers build a navigable outline from the headings on a page, and users jump between them to find a section. A heading with nothing to announce becomes a blank rung in that ladder: it is counted, it is landed on, and it says nothing.
Headings usually end up empty for one of two reasons. A <h2> is used as a styling hook around an image or an icon, or a heading is rendered before its content arrives and left in place when the content does not.
What squirrel checks
One check, empty-heading, at severity warning. It reads every <h1> through <h6>, skipping headings that are not rendered, since a screen reader never announces them. squirrel reads that the same way as button-name and the other naming rules (see below), so a heading hidden by the hidden attribute, aria-hidden="true" on itself or an ancestor, display: none or a closed <dialog> is skipped and counted, and one a style may hide is unconfirmed.
Content is accepted from the heading’s text, a non-empty aria-label, an aria-labelledby pointing at an element with text, or a nested <img> with non-empty alt.
- Warn with
N empty heading(s) found. Items readh2#promo,h3.subtitleor a bare tag name, carry the heading’s opening tag as a snippet, and say which heading on the page, in document order, they are (for exampleh2, heading 5 of 12 on the page), so two empty headings of the same level can be told apart. Up to ten are listed, with a count of the rest. - Pass with
All headings have content. - Info with
No headings found.
Hidden headings
An empty heading out of the accessibility tree has nothing to announce, so it is not reported: one with the hidden attribute (except hidden="until-found"), aria-hidden="true" on itself or an ancestor, display: none on itself or an ancestor (set inline or by a <style> rule squirrel can decide), or inside a closed <dialog> or an unopened popover. The pass message then reads “in the accessibility tree” and the count of skipped elements is kept in hiddenWithoutName. It still needs a name before it is shown.
squirrel reads the markup, inline styles and the page’s <style> blocks, the same way as form-labels; see form-labels for what it can and cannot decide. External stylesheets are not read. An unnamed heading that carries a no-js class, itself or on an ancestor other than <html>, is reported as unconfirmed on a page with an external stylesheet or an @import, because a theme usually hides that fallback once JavaScript runs. One a style may hide but squirrel cannot settle (a pseudo-class, a media query, a page that also loads an external stylesheet) is unconfirmed too. Report message: N empty heading(s) may be hidden by a style (unconfirmed), under the check name empty-heading-unconfirmed.
A heading with hidden="until-found" stays reported, because its content remains findable. An inline style squirrel cannot read plainly (a comment, a multi-keyword display) is unconfirmed rather than decided.
How to fix it
<h2>
<img src="/logo-partner.svg" alt="Northwind Traders">
</h2>Give the heading text, or a name through alt on the image inside it. A heading that only exists for its styling is better written as a <div> or a <p>, which keeps the visual design and takes the blank entry out of the outline.
A heading whose text is supplied by CSS, through content on a pseudo-element, is reported. That is correct: pseudo-element text is not reliably exposed to assistive technology, so the fix is to put the words in the markup.
| Rule ID | a11y/empty-heading |
| Category | Accessibility |
| Scope | Per-page |
| Severity | warning |
| Weight | 5/10 |
Enable / disable
Disable this rule
[rules]
disable = ["a11y/empty-heading"]Disable all Accessibility rules
[rules]
disable = ["a11y/*"]Enable only this rule
[rules]
enable = ["a11y/empty-heading"]enable replaces the default ["*"], so this is the only rule selected. Leave disable out: a rule that matches any disable pattern is skipped even when enable names it. Only an enabled setting under a rule’s own [rule_options] table overrides both lists.
Related rules
- a11y/heading-order: levels that skip, once the headings have content.
- content/heading-hierarchy: the same outline judged for structure and SEO.
- core/h1: whether the page has a single top-level heading at all.
- a11y/aria-text:
role="text"containers that swallow focusable elements, a role sometimes reached for on split headings.
Accessibility findings ship in every audit next to the SEO, performance and agent experience rules. See Website security scan with AI for how an agent works through a report.
References
- The heading elements, MDN
- Understanding SC 2.4.6 Headings and Labels, W3C
- H42: Using h1-h6 to identify headings, W3C
Check your site
Run squirrel audit https://example.com and open the Accessibility section of the report. Each blank heading is listed by level and by id or class. Signed-out local audits need no account and cost nothing. Signed-in audits spend credits.