Accessibility pattern · Layout and structure

Heading structure

Screen reader users skim a page by its headings, the way sighted readers scan the bold lines. A heading's level says where its section sits in the outline, not how big the text is: CSS decides the size.

WCAG criteria
6
Keyboard rules
4
Checked with
axe, keyboard and the inspector

Live demo

Try it

Use it with a mouse, a keyboard or a screen reader. The inspector beside it shows what the browser tells assistive technology as you go: focus, state changes and announcements.

Headings

0 found

H1 This page's own name, above the demo

    Every level follows the one before it.

    A weekend in Udaipur

    Lakes, palaces and rooftop dinners: two days are enough for the old city, if you plan the mornings.

    Getting there

    By train

    Overnight trains from Delhi reach Udaipur City station early in the morning.

    By air

    The airport is about 22 km from the old city, a 40-minute taxi ride.

    Where to stay

    Haveli guesthouses near Gangaur Ghat are a short walk from Lake Pichola.

    What to eat

    Dal baati churma at a family thali place, and kachori near Jagdish Temple for breakfast.

    The demo works without JavaScript only as far as its HTML does; the inspector needs JavaScript.

    Keyboard

    Keys it answers to

    Every action works without a pointer.

    KeyWhat it does
    TabMoves to Show structure, then through the headings list.
    Enter or SpaceOn an entry in the headings list, moves focus to that heading in the article; the next Tab carries on from there.
    SpaceOn Show structure, labels every heading in the article with its level, and marks bold text that only looks like one.
    H or 1 or 2 or 3Screen reader keys, not the page's: H moves to the next heading and a number to the next heading at that level, in NVDA and JAWS. The markup is all they need.

    Screen readers

    What it announces

    Written from the roles, names and states in the markup.

    WhenExpected announcement
    Enter on H3 Getting there in the listGetting there, heading, level 3
    A screen reader lists the headingsA weekend in Udaipur, level 2; Getting there, level 3; By train, level 4; By air, level 4; Where to stay, level 3; What to eat, level 3
    Broken version: the list reaches Getting thereH4 Getting there, Level jumps from 2 to 4, button
    Broken version: a screen reader reads By trainBy train (plain text: H skips it, and it is missing from the headings list)
    Show structure is switched onShow structure, switch, on (checked)

    These are expected announcements, not recordings. Wording and order differ between screen readers and browsers.

    Code

    Copy the code

    The exact files this demo runs on. The styles are served with a prefix that keeps this site's own styles out of the demo; what you copy is the original.

    The markup

    <div class="ap-hd" data-ap-headings>
      <div class="ap-hd__rotor">
        <button type="button" class="ap-hd__switch" role="switch" aria-checked="false" data-hd-structure>
          <span class="ap-hd__track" aria-hidden="true"><span class="ap-hd__knob"></span></span>
          Show structure
        </button>
        <div class="ap-hd__rotor-top">
          <p class="ap-hd__rotor-name" id="hd-rotor-name">Headings</p>
          <p class="ap-hd__count"><span data-hd-count>0</span> <span>found</span></p>
        </div>
        <p class="ap-hd__context"><span class="ap-hd__lvl" translate="no">H1</span> <span>This page's own name, above the demo</span></p>
        <ol class="ap-hd__list" role="list" aria-labelledby="hd-rotor-name" data-hd-list></ol>
        <div class="ap-hd__fakes" data-hd-fakes hidden>
          <p class="ap-hd__fakes-name" id="hd-fakes-name">Looks like a heading, but is not</p>
          <ul class="ap-hd__fake-list" role="list" aria-labelledby="hd-fakes-name" data-hd-fake-list></ul>
        </div>
        <p class="ap-hd__verdict" data-hd-verdict>
          <svg class="ap-hd__verdict-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path class="ap-hd__ok" d="m5 12 5 5L20 7"/><path class="ap-hd__bad" d="M12 8v5m0 3.5v.5"/></svg>
          <span data-hd-verdict-text>Every level follows the one before it.</span>
          <span data-hd-problems hidden></span>
        </p>
      </div>
    
      <article class="ap-hd__article" data-hd-good>
        <h2 class="ap-hd__h2">A weekend in Udaipur</h2>
        <p class="ap-hd__byline">By Meera Iyer · 6 min read</p>
        <p>Lakes, palaces and rooftop dinners: two days are enough for the old city, if you plan the mornings.</p>
        <h3 class="ap-hd__h3">Getting there</h3>
        <h4 class="ap-hd__h4">By train</h4>
        <p>Overnight trains from Delhi reach <a href="#hd-station">Udaipur City station</a> early in the morning.</p>
        <h4 class="ap-hd__h4">By air</h4>
        <p>The airport is about 22 km from the old city, a 40-minute taxi ride.</p>
        <h3 class="ap-hd__h3">Where to stay</h3>
        <p><a href="#hd-havelis">Haveli guesthouses</a> near Gangaur Ghat are a short walk from Lake Pichola.</p>
        <h3 class="ap-hd__h3">What to eat</h3>
        <p>Dal baati churma at a family thali place, and kachori near Jagdish Temple for breakfast.</p>
      </article>
    
      <!-- The broken version, for comparison only: the same classes, so it
           looks the same, but the tags are wrong. Do not copy this one. -->
      <article class="ap-hd__article" data-hd-broken hidden>
        <h2 class="ap-hd__h2">A weekend in Udaipur</h2>
        <h5 class="ap-hd__byline">By Meera Iyer · 6 min read</h5>
        <p>Lakes, palaces and rooftop dinners: two days are enough for the old city, if you plan the mornings.</p>
        <h4 class="ap-hd__h3">Getting there</h4>
        <p class="ap-hd__h4"><b>By train</b></p>
        <p>Overnight trains from Delhi reach <a href="#hd-station">Udaipur City station</a> early in the morning.</p>
        <p class="ap-hd__h4"><b>By air</b></p>
        <p>The airport is about 22 km from the old city, a 40-minute taxi ride.</p>
        <h3 class="ap-hd__h3">Where to stay</h3>
        <p><a href="#hd-havelis">Haveli guesthouses</a> near Gangaur Ghat are a short walk from Lake Pichola.</p>
        <h3 class="ap-hd__h3">What to eat</h3>
        <p>Dal baati churma at a family thali place, and kachori near Jagdish Temple for breakfast.</p>
      </article>
    </div>
    

    WCAG 2.2

    What it meets

    The success criteria this pattern takes care of, and how.

    • 1.3.1 Info and Relationships Level A

      Section names are real h2 to h4 elements, so the outline people see is the outline assistive technology gets; nothing is only bold.

    • 2.4.1 Bypass Blocks Level A

      Screen reader users move from heading to heading to skip what they do not need, one of the techniques WCAG lists for bypassing blocks.

    • 2.4.6 Headings and Labels Level AA

      Each heading names its section (Getting there, Where to stay), so it makes sense on its own in a headings list.

    • 2.4.10 Section Headings Level AAA

      The article is organised by section headings at levels that never skip, so the outline holds when read out of context.

    • 2.4.3 Focus Order Level A

      Choosing a heading from the list moves focus to it, so the next Tab continues from that section.

    • 4.1.2 Name, Role, Value Level A

      Show structure is a button with role=switch and aria-checked, so its name, role and on or off state are exposed.

    Usage

    When to use it

    Use it

    • Any page with more than one section: articles, settings pages, product pages, help pages.
    • Components that start a section, like a card group or a dialog: give them a heading at the level where they are placed.

    Use something else

    • Text that does not start a section, like a byline, a price or a tagline, however big or small it is drawn.
    • Headings for every card in a long list, when the list itself is the only thing people jump to.

    Common failures

    How it usually goes wrong

    • Skipping levels for the look

      Going from h2 to h4 because h4 is the size you wanted breaks the outline. Pick the level by position and set the size with a class.

    • Bold text as a heading

      A bold paragraph looks like a heading but is read as plain text and never appears in the headings list, so its whole section is hidden from people moving by H.

    • A heading chosen for its size

      An h5 byline or an h2 price puts noise in the headings list. If it does not start a section, it is a paragraph with a class.

    • A fixed level inside a component

      A card that always renders h3 lands under an h4 somewhere else. Let whoever places the component choose its level.

    • Empty or icon-only headings

      A heading with no text, or only an icon, is announced as heading level 3 and nothing else. Every heading needs words.

    • No h1, or several

      The page's name belongs in one h1. Components dropped into a page should not bring their own.

    Notes

    Building it

    • The article starts at h2 because this page's own name is its h1. On your page, start where the content sits: one h1 for the page's name, h2 for its main sections, and so on down.
    • Style by class, not by tag: the broken version uses the same classes, so both look alike and only the tags differ, which is exactly why the problems go unnoticed.
    • The headings list is built from the markup the way a screen reader builds its own, and checks what a reviewer would: level jumps against the nearest higher heading, headings set smaller than the body text, and bold lines that look like headings.
    • aria-level on a heading changes only what is announced. Change the element instead, so the page, the outline and the styles agree.
    • A heading reached from the list gets tabindex="-1" for the jump only, so a later click on it does not draw a focus ring.

    Sources: WAI tutorial: Headings · WCAG technique H42: h1 to h6 for headings · Understanding 2.4.10 Section Headings

    Checked with axe in light and dark themes, at desktop and phone widths, and by keyboard. Not yet tested with every screen reader and browser pair. Report a correction