( Perceivable / WCAG 1.3.1 )

Heading levels are skipped

ModerateLevel AWCAG 1.3.1 — Info and Relationships

What is this issue?

A page’s heading elements, read in document order, include a jump of more than one level between one heading and the next, most commonly <h1> straight to <h3>, or <h2> straight to <h4>. Each heading level from <h1> to <h6> is meant to represent one step of nesting depth; a level skip means the document’s actual outline and its announced outline no longer agree with each other.

Dropping down is never a problem: an <h4> followed by an <h2> simply starts a new top-level section, which is valid. The failure is specifically an increase of more than one level at once, since that’s the only direction that leaves a gap a screen reader has no way to fill in.

Why does this matter?

Sighted users infer a page’s hierarchy from a mix of signals (font size, indentation, whitespace, position), often without consciously noticing the underlying heading levels at all. Screen reader users get exactly one signal: the number announced with each heading. When a <h2> is followed by an <h4>, the screen reader says “heading level 4,” and the user has to conclude they’ve entered a sub-sub-section nested two levels deep, because that’s what the number tells them, even though visually and structurally it’s just the next sub-topic under the same <h2>.

That misread compounds across a page. A user who thinks they’re deep inside a minor aside skips past sections they’d have opened if the level had told them the truth, because the whole point of navigating by heading level is to use depth as a proxy for importance. A skipped level breaks that proxy silently, with no visual cue that anything is wrong.

Who is affected?

  • Screen reader users: hear an inflated heading level that misrepresents how deeply nested a section actually is, and may skip content they’d judge more important if the level matched the real structure.
  • Cognitive disabilities: users building a mental outline of the page from heading levels get an outline that doesn’t match reality, adding confusion on top of whatever the content itself already requires processing.

What users experience

Wei uses NVDA on Windows to scan a long annual report for the “Regional breakdown” figures, moving heading by heading using the H key. The report goes <h1>Annual Report 2026</h1>, <h2>Financial Summary</h2>, then straight to <h4>Regional Breakdown</h4> with no <h3> in between. NVDA announces “heading level 4,” so Wei assumes this is a minor sub-point buried under several layers and keeps pressing H past it, looking for a heading that sounds more like a main section, passing over the exact figures he needed because the level told him it wasn’t important.

How do I fix it?

Use heading levels in order, one step at a time, matching the real nesting of your content: an <h2> section’s first sub-topic gets an <h3>, never an <h4> or lower. This works because the level number is the only nesting signal a screen reader passes along; keeping it consecutive keeps the announced structure identical to the actual structure.

If a heading needs to look smaller or larger than its correct level implies, change that with CSS, not by picking a different tag. An <h3 class="text-sm"> that looks like a caption is fine; an <h5> chosen only because it renders at the right font size is not, because it corrupts the level for anyone who can’t see the font size at all. Audit component libraries specifically: a reusable “card” or “accordion” component that hardcodes its internal heading tag (say, always <h4>) will introduce a skip the moment it’s dropped into a section under an <h2> with no <h3> wrapper.

Code Examples

Before
<h1>Annual Report 2026</h1>
<h2>Financial Summary</h2>
<h4>Regional Breakdown</h4>
<!-- skipped h3 -->
After
<h1>Annual Report 2026</h1>
<h2>Financial Summary</h2>
<h3>Regional Breakdown</h3>

The content and visual position didn’t need to change at all; only the tag. Renumbering the “Regional Breakdown” heading from <h4> to <h3> realigns the announced level with the actual nesting depth: one step under “Financial Summary,” which is itself one step under the page’s <h1>.

Common Mistakes

Mistake: “I picked <h4> because it’s the font size I wanted, and it’s still technically a heading.” Choosing a heading tag for its default font size instead of its nesting depth is the single most common cause of this rule. The fix is to keep the tag matched to depth and override the visual size with a CSS class: decouple what the heading looks like from what level it structurally is.

Mistake: “The visual design already shows the hierarchy clearly, so the heading levels are just a technical detail.” Font size, indentation, and spacing are entirely invisible to a screen reader user; the heading level is the only hierarchy signal that reaches them. A visually obvious hierarchy and a correct heading structure are two separate things, and only one of them helps a screen reader user.

Mistake: “A reusable component always renders the same heading tag, so it can’t be the source of a skip.” A component that hardcodes, say, <h4> for its internal title is fine in isolation, but the moment it’s placed directly under a page’s <h2> with no <h3> wrapping it, it creates exactly this skip. Components that render a heading should accept the heading level as a prop from whatever page uses them, not hardcode one.

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 in document order and flags any heading whose level jumps by more than one step above the heading immediately before it: for example, an <h2> directly followed by an <h4>.

False negative: the check only compares each heading to the one immediately before it, so a page whose very first heading is already an <h3> (skipping <h1> and <h2> from the start) has nothing earlier to compare against and isn’t flagged. It also only inspects native <h1><h6> elements taken from a single scan snapshot, so a role="heading" element with aria-level set, or a heading injected into the page later by client-side JavaScript after the scan already ran, is never evaluated. False positive: a third-party widget embedded in your page (a payment form, a chat panel) that renders its own internal heading hierarchy can trigger this check relative to your page’s surrounding headings, even though you don’t control its markup directly; the underlying accessibility problem is still real, but the fix may not be yours to make. Manual step: confirm the page’s very first heading is an <h1> (not caught automatically), and review any flagged skip to see whether it originates from your own markup or an embedded third-party component.

Manual Testing

  1. Open the page in Chrome or Firefox with NVDA or JAWS running on Windows.
  2. Open the screen reader’s heading list (NVDA: Insert+F7, filtered to Headings; JAWS: Insert+F6) to see every heading’s level laid out in order.
  3. Read down the list and confirm each level increases by exactly one step at a time from the previous heading, never a jump of two or more.
  4. Separately confirm the page starts with a single <h1> before any other heading appears, since a level check alone won’t catch a page that starts at <h3> with no earlier heading to compare against.
  5. If any level jumps by more than one step going down the list, the check fails at that heading.

1.3.1 Info and Relationships: Information, structure, and relationships conveyed through presentation must also be available programmatically. A heading level skip is exactly this kind of failure: the intended relationship between a section and its sub-section is implied by design but not correctly represented in the heading structure a screen reader reads.

Heading is empty or contains only whitespace and Heading doesn’t describe its section cover the other two ways heading navigation breaks (no text at all, or text too generic to be useful), completing the set of failures that undermine the same heading-jump workflow this rule protects.

List items are not contained within a list element and Definition list structure is invalid are the same “structure implies a relationship that the markup doesn’t back up” failure applied to lists and term-definition pairs instead of headings.

Form input has no label shares the root cause across the whole 1.3.1 criterion: presentation alone (visual size, visual proximity) is never enough; the relationship has to exist programmatically for assistive technology to read it.

References

Frequently asked questions

Is skipping from h1 to h3 worse than skipping from h4 to h6?

Both are equally invalid structurally, but a skip near the top of the hierarchy usually affects more content, since an h2 typically introduces a larger section than an h5. Fix every skip regardless of where it happens in the hierarchy.

Can I go back down levels, like from h4 to h2, without it counting as a skip?

Yes. Heading levels can decrease by any amount, since that just means a new, higher-level section is starting. The rule only applies to increases of more than one level at a time, such as h2 directly to h4.

Does this rule apply if I use role="heading" and aria-level instead of h1 to h6 tags?

The same principle applies, though RedFlag current automated check only walks native h1 to h6 elements. A role="heading" element with aria-level="4" placed directly after a native h2 has the same skip problem for screen reader users, even though it will not be caught by this specific automated check.

Why does a heading skip matter if the visual design already shows the hierarchy with size and spacing?

Because screen reader users cannot see font size or spacing at all. The heading level is the only signal of hierarchy that reaches them, so when the level sequence does not match the real structure, they get no accurate map of the page regardless of how clear the visual design looks.

Does axe-core have an equivalent check for this?

Not exactly the same one. Axe-core has historically avoided a strict sequential heading-level rule because valid exceptions exist in some edge cases; RedFlag runs its own custom detector instead, comparing each heading to the one immediately before it in document order.