Heading Hierarchy for A11y
Use one h1 and nested h2–h6 as a document outline — skip levels carefully, don’t style divs as headings, and help SR navigation.
- accessibility
- heading-hierarchy
Screen reader users jump by headings the way sighted users scan bold titles. A page with random h3s for font size — or no headings at all — removes that map. Hierarchy is structure, not typography.
Docs: WAI Headings, WCAG 1.3.1 / 2.4.6.
Rules of thumb
- One main
h1per page/view (product SPA: one per route). - Nest without chaos:
h1→h2→h3… - Don’t pick heading level for visual size — use CSS.
- Don’t skip multiple levels without reason (
h2→h5). - Don’t use headings only for bold text inside paragraphs.
<h1>Account settings</h1>
<h2>Profile</h2>
<h2>Security</h2>
<h3>Password</h3>
<h3>Two-factor auth</h3>
<h2>Billing</h2>
Visual size vs level
.page-title {
font-size: 2rem;
} /* on h1 */
.section-title {
font-size: 1.25rem;
} /* on h2 */
<!-- Bad: fake heading -->
<div class="section-title">Security</div>
<!-- Bad: wrong level for style -->
<h5 class="hero">Welcome</h5>
SPAs and client routing
On navigation:
- Update
document.title - Move focus to
h1ormain - Ensure the new view’s heading outline makes sense
See focus management, language and page titles.
Cards and components
Card titles are often h2 or h3 depending on page context. Design-system Card.Title should accept a level prop:
function CardTitle({
as: Tag = 'h2',
children,
}: {
as?: 'h2' | 'h3' | 'h4';
children: React.ReactNode;
}) {
return <Tag className="card-title">{children}</Tag>;
}
Tools
- Headings map in browser extensions (WAVE, headingsMap)
- axe heading-order rules (catch some skips)
- Manual SR rotor (VoiceOver/NVDA headings list)
Footguns
- Multiple
h1s for layout columns. - Icon fonts as the only “title.”
- Accordion headers that aren’t headings or buttons correctly.
- PDF-like marketing pages with no outline.
Interview out-loud answer
“Headings create the document outline for AT. One h1, nested levels by structure not CSS size, and real heading elements — not styled divs. Components take a level prop so cards fit the page outline.”
Skipped levels in the wild
Going h2 → h4 because the design “looks smaller” is the usual failure. AT users may wonder if they missed a section. Style an h2 smaller — don’t pick h4 for cosmetics.
Headings inside components
Modals, drawers, and cards often ship with a hard-coded h2. Nested on a page that already used h2 for sections, the outline gets noisy. Accept a level prop:
type TitleProps = { level?: 1 | 2 | 3 | 4; children: React.ReactNode };
function Title({ level = 2, children }: TitleProps) {
const Tag = `h${level}` as keyof JSX.IntrinsicElements;
return <Tag className="title">{children}</Tag>;
}
Empty headings and icon-only
<!-- Bad -->
<h2><svg aria-hidden="true">…</svg></h2>
Headings need accessible text. Provide visible text or an accessible name SR can announce.
Outline as a review artifact
For content-heavy PRs, paste the heading outline from a browser extension. If the outline doesn’t tell the story of the page, fix structure before shipping copy polish.
Production checklist
Before you call this done in a PR, verify you can explain the concept in two sentences, show one code sample from memory, and name a failure mode you personally have seen. Link related docs in the review so future readers can go deeper without rediscovering primary sources.
Common review comments
Reviewers should ask: Does this change preserve edge-case behavior? Is the public API still honest under strict TypeScript or under real assistive tech? Are tests asserting user-visible outcomes rather than implementation trivia? Capture the answer in the PR description when the tradeoff is non-obvious.
Related on this site
- Language and page titles
- What is the accessibility tree
- Skip links
- WCAG principles POUR
- Focus management
Further reading
Related guides
- Accessible Combobox PatternBuild or evaluate comboboxes with APG keyboard behavior, aria-expanded/activedescendant, and filterable listbox pairing.
- Accessible Forms ErrorsLabel inputs, associate errors with fields, announce failures, and avoid placeholder-only forms that break accessibility.
- Accessible Menus PatternAPG menu and menubar keyboard model — arrow navigation, escape to close, aria-expanded, and when a disclosure is enough.
- Accessible Modals PatternsModal dialogs that work: focus trap, Escape, return focus, aria-modal, and why native dialog or APG patterns beat div soup.
- Accessible Names ComputationHow browsers compute accessible names from content, labels, aria-label, and labelledby — and how that powers Testing Library queries.