Screen reader users rely on headings to navigate your page. Skipped levels break this navigation and make your content structure unclear.
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.
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
})
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>
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 */
}
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.
<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>
<Card>
<CardHeading :level="2">Products</CardHeading>
<Card>
<CardHeading :level="3">Electronics</CardHeading>
</Card>
</Card>
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>
)
}
[...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.
<p> or <span> with CSS. Reserve headings for actual document structure.<h1> per page representing the main topic.<h1>. Starting at a lower level implies missing parent sections.<div role="heading" aria-level="2"> works but is more fragile than native <h2>. Use real heading elements.display: none which hides it from screen readers too.Heading order issues often appear alongside:
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.