Skip to main content
CodeRocket
Accessibilitycriticaldocument-structure

Use logical heading hierarchy

rule · heading-hierarchy

Headings create a document outline that screen readers use for navigation—proper hierarchy is essential.

Code Example

HTML
<!-- ✅ Good: Logical hierarchy -->
<h1>Web Accessibility Guide</h1>
 
<h2>Getting Started</h2>
<p>Introduction content...</p>
 
<h3>Prerequisites</h3>
<p>What you need to know...</p>
 
<h3>Tools</h3>
<p>Recommended tools...</p>
 
<h2>Core Principles</h2>
<p>Main principles content...</p>
 
<h3>Perceivable</h3>
<p>Content about perceivability...</p>

Why It Matters

Screen reader users navigate by headings like a table of contents—skipping levels or poor hierarchy makes content impossible to understand and navigate.

Common Mistakes

HTML
<!-- ❌ Bad: Multiple h1 elements -->
<h1>Company Name</h1>
<h1>Product Page</h1>
 
<!-- ❌ Bad: Skipping heading levels -->
<h1>Main Title</h1>
<h3>This should be h2</h3>
 
<!-- ❌ Bad: Using headings for styling -->
<h4>I just want this text to be smaller</h4>
 
<!-- ❌ Bad: Non-descriptive headings -->
<h2>Section 1</h2>
<h2>Click Here</h2>

Framework Examples

React Component

TSX
interface SectionProps {
  title: string
  level?: 1 | 2 | 3 | 4 | 5 | 6
  children: React.ReactNode
}
 
function Section({ title, level = 2, children }: SectionProps) {
  const Heading = `h${level}` as keyof JSX.IntrinsicElements
 
  return (
    <section aria-labelledby={`heading-${title.toLowerCase().replace(/\s/g, '-')}`}>
      <Heading id={`heading-${title.toLowerCase().replace(/\s/g, '-')}`}>
        {title}
      </Heading>
      {children}
    </section>
  )
}

Next.js Page Structure

TSX
export default function BlogPost({ post }) {
  return (
    <article>
      <h1>{post.title}</h1>
 
      <h2>Introduction</h2>
      <p>{post.intro}</p>
 
      {post.sections.map((section, index) => (
        <section key={index}>
          <h2>{section.title}</h2>
          <div dangerouslySetInnerHTML={{ __html: section.content }} />
 
          {section.subsections?.map((sub, subIndex) => (
            <div key={subIndex}>
              <h3>{sub.title}</h3>
              <p>{sub.content}</p>
            </div>
          ))}
        </section>
      ))}
    </article>
  )
}

Visual Styling Without Headings

CSS
/* Style text without semantic heading */
.section-label {
  font-size: 1.25rem;
  font-weight: bold;
  color: #333;
}
HTML
<!-- Use CSS class, not heading level for styling -->
<p class="section-label">This is styled text, not a heading</p>

Exceptions

  • Evaluate the rendered experience before treating a static-code smell as a blocker; interaction timing, browser behavior, and assistive technology output often determine severity.
  • Not every secondary accessibility issue deserves equal weight; prioritize the issue that most directly blocks perception, operation, or understanding.
  • Avoid adding redundant markup or ARIA solely to satisfy a rule when a simpler semantic implementation would eliminate the issue entirely.

Standards

  • Align the implementation with W3C WAI: WCAG Overview and verify the rendered experience, not only the source code.
  • Align the implementation with MDN: Accessibility and verify the rendered experience, not only the source code.

Verification

Automated Checks

  • Use browser DevTools to list all headings (HeadingsMap extension)

Manual Checks

  • Verify outline shows logical structure without skipped levels
  • Check there's exactly one h1
  • Screen reader: navigate headings with H key to verify order