squirrelscan
GitHub

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 read h2#promo, h3.subtitle or 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 example h2, 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 IDa11y/empty-heading
CategoryAccessibility
ScopePer-page
Severitywarning
Weight5/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.

  • 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

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.

Type to search…

↑↓ navigate↵ openesc close