Skip to main content

Fix Heading Hierarchy: A Practical Guide to Accessible Headings

Headings are one of the most important structural elements for accessibility. Screen reader users rely on headings to understand page structure and navigate content quickly — pressing the H key in screen readers like NVDA, JAWS, and VoiceOver jumps between headings, and users can pull up a list of all headings to scan a page in seconds. When heading hierarchy is broken, this navigation method fails, and the page becomes difficult to understand.

WCAG Requirements and Axe-Core Rules

Heading hierarchy is covered by two WCAG success criteria:

  • WCAG 1.3.1 Info and Relationships (Level A) — Information, structure, and relationships conveyed through presentation must be programmatically determinable. Headings must use proper heading elements (<h1> through <h6>), not just styled text.
  • WCAG 2.4.6 Headings and Labels (Level AA) — Headings and labels must describe the topic or purpose of the content they introduce.

Axe-core flags heading problems with three specific rules:

  • heading-order — Heading levels should increase by one. An <h4> should not follow an <h2> without an <h3> in between.
  • empty-heading — Heading elements must contain visible text. An <h2> with no content or only whitespace is flagged.
  • page-has-heading-one — The page should contain an <h1> element. This provides the top-level description of the page content.

Correct Heading Hierarchy

A well-structured page follows a strict hierarchy. Use exactly one <h1> for the page title. Divide major sections with <h2> elements. Subdivide within those sections using <h3>, and so on. Never skip levels — do not jump from <h2> to <h4>.

<h1>Product Catalog</h1>

  <h2>Electronics</h2>
    <h3>Laptops</h3>
    <h3>Smartphones</h3>

  <h2>Clothing</h2>
    <h3>Men's</h3>
      <h4>Outerwear</h4>
      <h4>Shirts</h4>
    <h3>Women's</h3>

Think of headings as a table of contents. If you extracted every heading from the page and listed them with indentation, the result should make sense as an outline of the content.

Fix: Skipped Heading Levels

This is the most common heading violation. It usually happens when developers choose heading levels for visual size rather than document structure.

Before (incorrect):

<h1>Our Services</h1>
<h4>Web Development</h4>   <!-- Skips h2 and h3 -->
<p>We build modern websites...</p>
<h4>Mobile Apps</h4>
<p>Native and cross-platform...</p>

After (correct):

<h1>Our Services</h1>
<h2>Web Development</h2>
<p>We build modern websites...</p>
<h2>Mobile Apps</h2>
<p>Native and cross-platform...</p>

If the <h2> looks too large visually, use CSS to adjust the size — never choose a heading level based on how it looks.

Fix: Multiple H1 Elements

A page should have exactly one <h1>. Multiple <h1> elements confuse screen reader users about which heading represents the page's primary topic.

Before (incorrect):

<h1>Company Name</h1>       <!-- Site branding -->
<h1>About Us</h1>            <!-- Page title -->
<h1>Our Mission</h1>         <!-- Section heading -->

After (correct):

<header>
  <a href="/">Company Name</a>  <!-- Not a heading -->
</header>
<h1>About Us</h1>               <!-- One h1 for page title -->
<h2>Our Mission</h2>            <!-- Section is h2 -->

The site name in the header is typically a link, not a heading. Reserve <h1> for the actual content of the page.

Fix: Empty Headings

Empty headings appear in screen reader heading lists as blank entries, making navigation confusing. They usually result from leftover markup, CMS templates, or headings that contain only an icon or image without alt text.

Before (incorrect):

<h2></h2>

<h3>  </h3>   <!-- Contains only whitespace -->

<h2><img src="icon.svg"></h2>   <!-- Image without alt text -->

After (correct):

<!-- Remove the empty heading entirely, or add content -->
<h2>Features</h2>

<!-- If heading contains an image, add alt text -->
<h2><img src="icon.svg" alt="Features"></h2>

<!-- Or use text alongside the icon -->
<h2><img src="icon.svg" alt="" role="presentation"> Features</h2>

Fix: Headings Used for Styling

Developers sometimes use heading elements purely for their default bold, large styling — even when the text is not actually a heading. This pollutes the document outline and misleads assistive technology users.

Before (incorrect):

<h3>Note: Delivery takes 3-5 business days.</h3>

<h4>$49.99</h4>   <!-- Price is not a heading -->

After (correct):

<p><strong>Note: Delivery takes 3-5 business days.</strong></p>

<p class="price">$49.99</p>

Use CSS classes to achieve the visual style you want. Heading elements should only be used when the content genuinely introduces a new section.

Fix: Non-Heading Text Styled as a Heading

The opposite problem also occurs: text that looks like a heading visually but uses a <div>, <span>, or <p> with large bold styling. Screen reader users will not find these in their heading list and will miss sections of content.

Before (incorrect):

<div style="font-size: 24px; font-weight: bold;">Customer Reviews</div>

After (correct — option 1, use a proper heading element):

<h2>Customer Reviews</h2>

After (correct — option 2, when you cannot change the element):

<div role="heading" aria-level="2">Customer Reviews</div>

Option 1 is always preferred. Use role="heading" with aria-level only when you genuinely cannot modify the HTML element — for example, when working with a third-party widget that injects its own markup.

Headings in Navigation and Sidebars

Navigation sections and sidebars are part of the page and their headings are part of the document outline. Use appropriate levels that fit the page hierarchy. If your <aside> is a sibling of your main content area, its heading should typically be an <h2>.

<main>
  <h1>Blog Post Title</h1>
  <p>Article content...</p>
</main>

<aside>
  <h2>Related Articles</h2>     <!-- Same level as main sections -->
  <ul>...</ul>

  <h2>Categories</h2>
  <ul>...</ul>
</aside>

If you do not want sidebar headings to be visible, use a visually hidden class rather than removing them. Screen reader users benefit from having labeled sections even when sighted users can infer meaning from layout.

<h2 class="sr-only">Sidebar Navigation</h2>

Heading Hierarchy in Cards and Components

Cards, modals, and reusable components introduce a common challenge: the correct heading level depends on where the component appears in the page. A card might need an <h3> on the homepage but an <h2> on a category page.

Before (incorrect — hardcoded heading level):

<!-- Card component always uses h3 -->
<div class="card">
  <h3>Card Title</h3>
  <p>Card description...</p>
</div>

After (correct — configurable heading level):

<!-- React example -->
function Card({ title, description, headingLevel = 'h3' }) {
  const Heading = headingLevel;
  return (
    <div className="card">
      <Heading>{title}</Heading>
      <p>{description}</p>
    </div>
  );
}

<!-- PHP example -->
<?php $tag = $headingLevel ?? 'h3'; ?>
<div class="card">
  <<?= $tag ?>><?= $title ?></<?= $tag ?>>
  <p><?= $description ?></p>
</div>

Pass the heading level as a prop or parameter so each page can supply the correct level for its context.

SPAs with Dynamic Headings

Single-page applications often replace page content without a full page reload. When a route changes, the heading hierarchy of the new view must stand on its own. Each view should have its own <h1>.

<!-- When the route changes, update the h1 -->
function PageView({ title, children }) {
  useEffect(() => {
    document.title = title;
    // Move focus to the heading for screen reader announcement
    const heading = document.querySelector('h1');
    if (heading) heading.focus();
  }, [title]);

  return (
    <main>
      <h1 tabIndex="-1">{title}</h1>
      {children}
    </main>
  );
}

Key considerations for SPAs:

  • Each view/route should contain exactly one <h1> describing that view
  • Update document.title to match the current view
  • Move focus to the new <h1> on route change so screen readers announce it (use tabIndex="-1" to make non-interactive elements focusable)
  • Ensure the heading hierarchy of each view is valid independently — do not rely on headings from persistent navigation or layout shells

CMS Fixes

Content management systems are a frequent source of heading problems. Content authors choose heading levels based on visual appearance, and templates may introduce their own headings that conflict with content headings.

WordPress: Many themes use <h1> for the site title in the header. Ensure your theme uses <h1> only for the post/page title. Educate editors to start body content at <h2> and not skip levels. Consider using a plugin like Yoast SEO which warns about heading hierarchy issues.

Generic CMS guidance:

  • Configure the rich text editor to limit available heading levels. If the template already provides <h1>, remove it from the editor toolbar to prevent duplicate <h1> elements.
  • Add validation that checks heading order before publishing. Warn authors when levels are skipped.
  • Provide a heading style guide with examples of correct usage for content editors.
  • If your CMS renders the page title as <h1> automatically, make sure the template does not also include another <h1> from the content body.

Testing with Heading Outline Tools

You can verify heading structure quickly with browser extensions and built-in tools:

  • HeadingsMap (Chrome/Firefox extension) — Displays a sidebar with the complete heading outline of the page, clearly showing the hierarchy and flagging skipped levels.
  • Web Developer toolbar — Use the "View Document Outline" feature to see the heading structure.
  • WAVE — The WAVE browser extension shows headings inline on the page with their level numbers, making skipped levels immediately visible.
  • axe DevTools — Automatically detects heading-order, empty-heading, and page-has-heading-one violations with specific element selectors so you can locate the problem fast.
  • Screen reader heading list — The most accurate test. In NVDA, press Insert + F7 and select the Headings tab. In VoiceOver on macOS, open the rotor with VO + U and navigate to Headings. In JAWS, press Insert + F6. This shows exactly what heading-navigation users experience.

A quick manual check: view your page's heading outline. If it reads like a coherent table of contents — one top-level title, logically nested sections, no gaps, no empty entries — your heading structure is correct.

Is your website accessible?

Scan your website for free and get your WCAG compliance score in minutes.

Scan your site free