Heading Order in Dynamically Loaded Content: What the Scanner Cannot See
A storefront's accessibility report showed a perfect heading structure. One h1, then h2s, then h3s, exactly as the standard wants. A screen reader user opened the filter panel and found an h3 sitting under nothing, because the panel loaded its own headings after the scan ran and the page never had a heading map that included them.
The heading map is a table of contents
Screen reader users navigate by headings the way sighted users scan a page. They pull up a list of headings, jump to the section they want, and skip the rest. That list is built from the DOM, in order, and it only works if the levels mean something: h1 for the page, h2 for the sections, h3 for the subsections. Skip a level, and the map has a gap. Put an h3 where an h2 belongs, and the user thinks the section is a subsection of something it is not. WCAG 1.3.1, Info and Relationships, and 2.4.6, Headings and Labels, are the criteria behind this, but the user experience is simpler than the numbers: a broken map means a broken page.
Where dynamic content breaks it
The static page is usually fine. The breakage lives in the states that appear after interaction. A modal that opens with an h2 when the page is already two h2s deep. A tab panel that injects h3s under an h1 with no h2 between them. An infinite scroll that appends h2s below the h1s of products that loaded earlier. A filter panel that re-renders its results and brings its own heading structure with it, one that was written for a different page entirely.
The pattern is always the same: the component was built in isolation, its headings made sense in the component, and nobody checked what they meant in the page. A tab component with an h2 for its title is reasonable on its own. Dropped into a page that already has three h2s, it is a fourth section the author never intended. The focus order problem in single-page apps is the same failure in a different costume: the order the user experiences is not the order the developer tested.
Why the scanner misses it
Axe, WAVE, and Lighthouse all scan the page as loaded. They see the DOM at rest, and the DOM at rest has a clean heading structure, because the dynamic content has not appeared yet. The failures only exist after a click, a filter, a scroll, and no static scan ever performs those actions. This is the same blind spot as CI thresholds that pass a build whose runtime states were never exercised: the tool is not wrong, it is just looking at a different page than the user is. A monitoring setup that only scans the loaded page will report a clean heading map forever, while users hit broken ones daily.
Test it the way a user hits it
Open the modal and pull the heading list. Switch the tab and pull it again. Apply a filter, load more results, open the mobile menu. After each interaction, the heading order should still read like a table of contents for the page the user is on. When it does not, the fix is usually small: change the level in the component, or move the component so its headings land under the right parent. The check takes minutes and it catches the class of failure the scanner is structurally unable to see.
Frequently asked questions
Is it okay to skip from h1 to h3 for visual reasons?
No. Heading levels should follow the structure of the content, and skipping from h1 to h3 breaks the outline a screen reader user navigates by. If the design needs a smaller-looking heading, style an h2 to look smaller instead of using an h3 for its size.
Does heading order follow the visual layout or the DOM?
Yes, if the visual order and the DOM order disagree, the heading map follows the DOM. A heading that looks like it belongs to one section but sits in the DOM under another will be announced in the wrong place. Fix the DOM order, not the heading level.
Can axe catch heading order problems?
Axe has a page-has-heading-one rule and flags empty headings, and it can catch skipped levels in the static DOM. It cannot catch headings that appear after interaction, because it only sees the page as loaded. Dynamic heading problems need a test that interacts first.
How often should I re-check heading order?
Run the same heading-order check after each interaction that changes the page: open the modal, switch the tab, apply the filter, load more results. The check is cheap and the failures cluster in exactly the states the static scan never reaches.