---
title: "Fix \"Heading elements are not in a sequentially-descending order\" without breaking your design"
slug: fix-heading-order-skipped-levels
published: 2026-10-29T13:19:00.14272+00:00
updated: 2026-10-29T13:19:13.035748+00:00
author: "M Asif Rahman"
author_url: https://ar.bd
category: "AI Readiness"
tags: check:E3, check:C3
description: "Fix the Lighthouse heading-order warning: find every skipped level with a console snippet, then change the tag and keep the look with a CSS class instead."
url: https://aiscan.site/blog/fix-heading-order-skipped-levels
---

Lighthouse prints "Heading elements are not in a sequentially-descending order" when a heading jumps more than one level, for example an `<h4>` straight after an `<h2>`. The fix is almost never to delete the heading. Change the tag to the level the outline needs and keep the look with a CSS class, which takes a few minutes per template. This guide finds every skip with a console snippet and then fixes the four sources that cause most of them.

It covers [check E3](/docs/checks/content) (heading hierarchy and server-rendered text) and the single-`<h1>` rule of C3, and it says plainly what AIScan scores and what only Lighthouse flags.

## Quick summary

| If you want to... | Do this | Time |
|---|---|---|
| See your outline and every skipped level | Paste the snippet below into the browser console | 1 minute |
| Fix a theme logo or site title that is an `<h1>` | Make it a `<p>` or `<div>` on every page except the home page | 5 minutes |
| Fix a widget or dialog with a hard-coded `<h4>` | Change the tag to the level its position needs, keep the size with a class | 5 minutes per widget |
| Keep the design while changing the tag | `<h2 class="h4">`: the tag carries meaning, the class carries the look | 0 |
| Confirm the fix | `npx aiscan-cli yoursite.com`, then read check E3 and the C3 `<h1>` count | 2 minutes |

## What the audit checks, and what AIScan checks

According to the HTML Living Standard, each heading after another one "must have a heading level that is less than, equal to, or 1 greater than" the previous heading's level ([HTML Standard, headings and outlines](https://html.spec.whatwg.org/multipage/sections.html#headings-and-outlines)). Going down is fine: an `<h3>` after an `<h5>` is not a skip. Going up by two or more is. According to Deque, its axe-core rule `heading-order`, which is the rule behind the Lighthouse audit (web.dev's old heading-order page now redirects to it), says the same: [heading levels should only increase by one](https://dequeuniversity.com/rules/axe/4.10/heading-order). Assistive technology users navigate by heading, and according to [WCAG 2.2 SC 1.3.1](https://www.w3.org/WAI/WCAG22/Understanding/info-and-relationships.html) structure shown visually has to be available in the markup too.

AIScan's checks are narrower. According to [our E3 study of 699 sites](/blog/server-rendered-text-headings-corpus) (9 October 2026): E3 counts words and `<h2>`/`<h3>` headings in the served HTML and does not read the `<h1>`; the `<h1>` belongs to C3, which wants exactly one. So an `<h2>` followed by an `<h4>` raises the Lighthouse audit and not an E3 finding, as long as the page still has its `<h2>` sections. | Case | Lighthouse (axe `heading-order`) | AIScan |
|---|---|---|
| `<h2>` then `<h4>`, with `<h2>` sections present | Flags the skip | No E3 finding |
| 150+ words, `<h3>` but no `<h2>` | Reads the order | E3 finding |
| Two `<h1>` elements | Outside the rule | C3 finding (wants exactly one) |
| Headings added by script | Reads the rendered DOM | Reads the served HTML |

A page with 150 or more words and `<h3>` headings but no `<h2>` does get an E3 finding. I did not re-scan a skipped-level test page for this guide: that behaviour comes from the study, so re-run your own scan to confirm it. Fix both anyway.

## Step 1: print the outline and flag every skip

Open the page, press F12, choose **Console**, and paste:

```js
(()=>{let p=0;const r=[...document.querySelectorAll('h1,h2,h3,h4,h5,h6')].map(h=>{const l=+h.tagName[1],f=p&&l>p+1?`  <- skips from h${p}`:'';p=l;return`${'  '.repeat(l-1)}h${l} ${h.textContent.trim().slice(0,40)}${f}`});console.log(r.join('\n')+`\nh1 count: ${document.querySelectorAll('h1').length}`)})()
```

I ran it in headless Chromium on a test page that had a logo `<h1>`, a cookie dialog `<h4>`, a second `<h1>` for the real title, an `<h2>` followed by an `<h4>`, and an `<h5>` under an `<h3>` (29 October 2026). Output:

```text
h1 Acme Store
      h4 Cookie settings  <- skips from h1
h1 Blue widgets
  h2 Specs
      h4 Weight  <- skips from h2
  h2 Fixed
    h3 ok
    h3 ok2
        h5 jump  <- skips from h3
    h3 Newsletter
h1 count: 2
```

Three skips and two `<h1>` elements, found in one pass. The snippet reads the rendered page. What a crawler that does not run JavaScript receives is the served HTML, so compare the two with `curl -s https://yoursite.com/ | grep -o '<h[1-6]' | tr '\n' ' '`. Headings that appear in the console but not in the curl output are added by script, which is the E3 problem in [our server-rendered text study](/blog/server-rendered-text-headings-corpus), not an ordering problem. [Fetching your site the way an agent does](/blog/fetch-site-as-ai-agent-curl) covers that test in full.

## Step 2: fix the four usual sources

| What you see in the outline | Cause | Fix |
|---|---|---|
| `<h1>` before the real `<h1>`, h1 count 2 | Theme wraps the logo or site title in `<h1>` | Make it a `<p>` or `<div>` on inner pages |
| `<h4>` or `<h3>` right after the first `<h1>` | Cookie banner, header widget or mega-menu with a fixed tag | Change the tag to match its place, or take it out of the heading outline |
| Levels chosen by size: `<h4>` used because it looks small | Page builder picks the tag from the font size | Set the tag and the size separately |
| Several jumps in the body text | Editors picked headings for their look | Re-level the headings, then style with a class |

**The theme logo.** A site title belongs in an `<h1>` only on pages where it is the page's subject. On WordPress block themes, select the **Site Title** block and change its level in the block toolbar or the **Settings** sidebar; its `block.json` in the Gutenberg repository defaults the level to 1 and lists 0 to 6 as options, and the paragraph option renders it as a `<p>` ([core/site-title block.json](https://github.com/WordPress/gutenberg/blob/trunk/packages/block-library/src/site-title/block.json)). Check the template for the page type that raised the count. On classic themes, change the wrapper tag in the theme's header template and keep a conditional so the home page can keep the `<h1>`.

**The hard-coded dialog heading.** A cookie dialog, popup or newsletter box often ships an `<h4>` because the vendor never knew where it would sit. A dialog that sits in the DOM before your `<h1>` makes the first heading on the page a level 4, as in the console output above. Options, in order: edit the template and change the tag, set `role="dialog"` with `aria-labelledby` pointing at a `<p>` title so it is not a heading, or load the dialog late so it never enters the first response.

**Style it with a class, not a tag.** Once the tag is right, the size is a styling job. According to Bootstrap's documentation, its `.h1` through `.h6` classes exist "for when you want to match the font styling of a heading but cannot use the associated HTML element" ([Bootstrap typography](https://getbootstrap.com/docs/5.3/content/typography/)). In any other stack, write the same two rules yourself:

```html
<h2 class="h4">Specs</h2>
```

```css
.h4 { font-size: 1.25rem; line-height: 1.3; }
```

The `<h2>` joins the outline at the right level and looks like the old `<h4>`. The class has no effect on the outline.

**Builders that pick the tag from the size.** In the page builder's heading widget, look for an HTML tag or semantic level setting next to the font size. Set the tag from the outline and the size from the design, separately. If the builder ties them together, use its text widget with the class above for the cosmetic cases.

## Step 3: verify

1. Paste the snippet again. There should be no `<-` lines and `h1 count: 1`.
2. Run the curl line from step 1 and confirm the served HTML shows the same order.
3. Scan the page with AIScan: `npx aiscan-cli yoursite.com`, and read the E3 evidence line and the C3 `<h1>` count. The `curl` equivalent is `curl -s https://yoursite.com/ | grep -c '<h1'`, expecting 1.
4. Run Lighthouse's accessibility audit in Chrome DevTools and confirm the heading-order item is gone.

## Maintenance

Widgets and theme updates bring skips back. Re-run the snippet on one page of each template after a theme or plugin update. [The AIScan CI gate](/blog/aiscan-ci-gate-github-actions) can fail a build when a check regresses. For the per-platform `<h1>` ownership table, see [one h1, title, meta description and JSON-LD by platform](/blog/title-meta-schema-by-platform). For writing the sections themselves, see [making a page answerable by AI](/blog/make-a-page-answerable-by-ai).

## Next step

Scan the page you just fixed at [aiscan.site](https://aiscan.site/), read check E3 and the `<h1>` count under C3, and browse the rest of the fix guides in [/guides](/guides).

