Fix Heading Order for Better Accessibility

Learn how to fix heading order issues in Lighthouse accessibility audits. Properly ordered headings convey semantic structure for easier navigation.
Harlan WiltonHarlan Wilton4 min read Published

Screen reader users rely on headings to navigate your page. Skipped levels break this navigation and make your content structure unclear.

What's Happening

Headings (<h1> through <h6>) should form a logical hierarchy, increasing by only one level at a time. When you skip from <h2> to <h4>, screen reader users wonder if they missed a section. They cannot tell if the skipped <h3> represents missing content or just poor markup.

Properly ordered headings let users jump between sections, understand the page structure, and orient themselves quickly.

Diagnose

Chrome DevTools

  1. Open DevTools (F12)
  2. Press Ctrl+Shift+P (Cmd+Shift+P on Mac)
  3. Type "Show headings" and select the option
  4. Review the heading outline - look for skipped levels

Quick console check:

const headings = [...document.querySelectorAll('h1, h2, h3, h4, h5, h6')]
let lastLevel = 0

headings.forEach((h) => {
  const level = Number.parseInt(h.tagName[1])
  if (level > lastLevel + 1 && lastLevel !== 0) {
    console.warn(`Skipped heading level: ${h.tagName} after H${lastLevel}`, h)
  }
  lastLevel = level
})

Accessibility Tree

  1. DevTools > Elements > Accessibility pane
  2. Expand the tree and look at heading structure
  3. Levels should descend sequentially: 1 > 2 > 3, not 1 > 3

Fix

1. Restructure Heading Levels

Fix the hierarchy by adjusting heading levels:

<!-- Before: Skipped level -->
<h1>Product Catalog</h1>
<h3>Electronics</h3>  <!-- Wrong: skipped h2 -->
<h3>Clothing</h3>

<!-- After: Sequential levels -->
<h1>Product Catalog</h1>
<h2>Electronics</h2>  <!-- Correct: h2 follows h1 -->
<h2>Clothing</h2>

For nested sections, continue the hierarchy:

<h1>Product Catalog</h1>
  <h2>Electronics</h2>
    <h3>Phones</h3>
    <h3>Laptops</h3>
  <h2>Clothing</h2>
    <h3>Shirts</h3>
    <h3>Pants</h3>

2. Style Independently from Semantics

Use CSS to achieve desired visual sizes without breaking semantics:

<!-- Keep correct heading level, style differently -->
<h2 class="text-3xl font-bold">Main Section</h2>
<h3 class="text-2xl font-semibold">Subsection</h3>
<h4 class="text-xl">Nested Item</h4>
/* Override default heading styles */
.section-title {
  font-size: 1.5rem;
  font-weight: 600;
}

h2.section-title,
h3.section-title,
h4.section-title {
  /* Same visual appearance, correct semantic level */
}

3. Fix Component Headings

Components often hardcode heading levels. Make them configurable:

<!-- Bad: Component always uses h3 -->
<div class="card">
  <h3>Card Title</h3>
</div>

<!-- Good: Heading level is contextual -->
<div class="card">
  <h2>Card Title</h2>  <!-- or h3, h4 depending on context -->
</div>

In component systems, pass the heading level as a prop.

Framework Examples

Nuxt / VueMake heading levels dynamic in components:
<script setup>
defineProps({
  level: {
    type: Number,
    default: 2,
    validator: v => v >= 1 && v <= 6
  }
})
</script>

<template>
  <component :is="`h${level}`" class="card-title">
    <slot />
  </component>
</template>
Usage:
<Card>
  <CardHeading :level="2">Products</CardHeading>
  <Card>
    <CardHeading :level="3">Electronics</CardHeading>
  </Card>
</Card>
ReactDynamic heading component:
function Heading({ level = 2, children, className }) {
  const Tag = `h${level}`
  return <Tag className={className}>{children}</Tag>
}

// Usage with context for automatic levels
function Section({ children }) {
  const parentLevel = useHeadingLevel()
  return (
    <HeadingLevelProvider value={parentLevel + 1}>
      {children}
    </HeadingLevelProvider>
  )
}

Verify the Fix

  1. Re-run Lighthouse - The "Heading elements appear in a sequentially-descending order" audit should pass
  2. Check heading outline - In DevTools, run:
[...document.querySelectorAll('h1,h2,h3,h4,h5,h6')]
  .map(h => `${h.tagName}: ${h.textContent.trim().slice(0, 50)}`)
  .join('\n')

The output should show logical nesting without skips.

  1. Screen reader test - Use heading navigation (H key in NVDA/JAWS) to move through headings. The structure should make sense.

Common Mistakes

  • Using headings for visual styling — If you want big bold text that isn't a section heading, use a <p> or <span> with CSS. Reserve headings for actual document structure.
  • Multiple h1 elements — While technically allowed in HTML5 with sectioning elements, most screen readers and SEO best practices recommend one <h1> per page representing the main topic.
  • Starting at h2 or h3 — The first heading on a page should typically be <h1>. Starting at a lower level implies missing parent sections.
  • Using divs with heading roles<div role="heading" aria-level="2"> works but is more fragile than native <h2>. Use real heading elements.
  • Hiding headings for visual design — If a section needs a heading for structure but you don't want it visible, use visually-hidden CSS, not display: none which hides it from screen readers too.

Heading order issues often appear alongside:

  • Document Title — The title and h1 should describe the same topic
  • Bypass — Proper headings enable skip navigation
  • HTML Lang — Both affect how screen readers interpret structure

Test Your Entire Site

Heading structure issues often creep in through reusable components, templates, or CMS content. A card component using <h3> might be correct on one page but break hierarchy on another. Unlighthouse scans every page and flags heading order violations across your entire site, so you can fix them at the source.