ESC

Type to search the knowledge base.

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.

beginner3 min read
  • 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

  1. One main h1 per page/view (product SPA: one per route).
  2. Nest without chaos: h1 → h2 → h3…
  3. Don’t pick heading level for visual size — use CSS.
  4. Don’t skip multiple levels without reason (h2 → h5).
  5. 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:

  1. Update document.title
  2. Move focus to h1 or main
  3. 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

  1. Multiple h1s for layout columns.
  2. Icon fonts as the only “title.”
  3. Accordion headers that aren’t headings or buttons correctly.
  4. 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.

Further reading

Related guides