HTML Module 2 – Text, semantics and page structure
Headings, paragraphs and the document outline
Structure a page with h1 to h6 so its outline reads like a table of contents, fix skipped heading levels, and use p, br, hr and hgroup for what they mean.
What you will learn
- Structure a page with h1 to h6 so that its outline reads like a table of contents
- Apply the HTML Standard's rule against skipping heading levels, and find skipped levels with a checker
- Use p, br, hr and hgroup for what they mean rather than how they look
- Explain why an h1 nested in a section is still a level-1 heading and is drawn at full size
Before you start
What you’ll learn · Before you start
What you will learn
- Structure a page with h1 to h6 so that its outline reads like a table of contents
- Apply the HTML Standard's rule against skipping heading levels, and find skipped levels with a checker
- Use p, br, hr and hgroup for what they mean rather than how they look
- Explain why an h1 nested in a section is still a level-1 heading and is drawn at full size
Before you start
On this page
Headings name the sections of a page. HTML has six heading elements, <h1> to <h6>, and the number in the name is
the heading’s level: an <h1> names the page or one of its top-level sections, an <h2> a section inside it, an
<h3> a part of that section, and so on down to <h6>. All the headings of a page, in the order they appear, form
its outline: the table of contents that browsers, search engines and screen readers build from your markup.
Paragraphs, the <p> element, hold the text between the headings.
Two rules from the HTML Standard keep an outline readable. A page that has headings should have at least one at level
1, and each heading may be at most one level deeper than the heading before it, so an <h1> followed directly by an
<h3> is an error. Pick each level for the structure of the content, never for its size: CSS sets the size.
The six heading levels
Here is a short guide marked up with three levels. The <h1> names the whole page, each <h2> names one of its
four topics, and each <h3> names a part of a topic:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Growing vegetables on a balcony</title>
</head>
<body>
<main>
<h1>Growing vegetables on a balcony</h1>
<p>A sunny balcony of two square metres can keep a small kitchen in greens for most of the year.</p>
<h2>Choosing containers</h2>
<p>Almost anything that holds soil and drains water will do.</p>
<h3>Size</h3>
<p>Leafy greens need about 15 cm of soil; tomatoes need at least 30 cm.</p>
<h3>Drainage</h3>
<p>Drill holes in the bottom and raise the pot on two bricks.</p>
<h2>Soil and compost</h2>
<p>Mix garden soil, compost and coco peat in equal parts.</p>
<h2>What to grow first</h2>
<h3>Leafy greens</h3>
<p>Spinach and fenugreek are ready to cut in four to six weeks.</p>
<h3>Tomatoes and chillies</h3>
<p>Start them from seedlings and give them six hours of sun a day.</p>
<h2>Watering in hot weather</h2>
<p>Water early in the morning, and feel the soil before you water again.</p>
</main>
</body>
</html> The browser draws each level in bold at its own default size. With no CSS of its own, a page gets these sizes in
Chromium 153 and Firefox 155: 32px for <h1>, 24px for <h2>, 18.72px for <h3>, 16px for <h4>, 13.28px for
<h5> and 10.72px for <h6> (Firefox reports some with more decimals, such as 18.7188px). They come from the
Rendering section of the Standard, which sets 2em, 1.5em, 1.17em, 1em, 0.83em and 0.67em of the 16px body text.
<h5> and <h6> come out smaller than the text around them, which is one reason authors are tempted to pick
levels by size.
A page may have more than one <h1>. The Standard itself shows a conforming page with three of them, one per fruit.
Most pages are about one thing, though, and then one <h1> that matches the page’s <title> gives everyone the
clearest start. The <title> itself is not a heading: it names the page in the tab and in search results, and it is
not part of the outline.
Reading a page’s outline
The Standard defines the outline as all the headings of a document in tree order, and says it should be used for things like tables of contents. You can build it yourself. This program reads the guide above, and a second version of it that comes up in the next section, picks out every heading and prints it indented by its level:
// Prints the outline of each page: every h1-h6 in the order it appears, indented by its level, the way a table of
// contents or a screen reader's list of headings shows them. It flags the two problems the HTML Standard names: a
// heading more than one level deeper than the heading before it (not allowed), and no heading at level 1.
// A regular expression is enough for these two small, valid files; a real checker reads the page with an HTML parser.
import { readFileSync } from "node:fs";
const PAGES = ["balcony-garden.html", "balcony-garden-by-size.html"];
const ENTITIES = { amp: "&", lt: "<", gt: ">", quot: '"', "#39": "'", nbsp: " " };
const textOf = (html) =>
html
.replace(/<[^>]+>/g, "")
.replace(/&(amp|lt|gt|quot|#39|nbsp);/g, (_, name) => ENTITIES[name])
.replace(/\s+/g, " ")
.trim();
function headings(html) {
const withoutComments = html.replace(/<!--[\s\S]*?-->/g, "");
return [...withoutComments.matchAll(/<h([1-6])\b[^>]*>([\s\S]*?)<\/h\1\s*>/gi)].map((m) => ({
level: Number(m[1]),
text: textOf(m[2]),
}));
}
for (const page of PAGES) {
const list = headings(readFileSync(page, "utf8"));
console.log(page);
let previous = null;
let skips = 0;
for (const h of list) {
let note = "";
if (previous && h.level > previous.level + 1) {
skips += 1;
const missing = [];
for (let level = previous.level + 1; level < h.level; level++) missing.push(`h${level}`);
note = ` <- skips ${missing.join(" and ")}: after an h${previous.level}, the next heading goes no deeper than h${previous.level + 1}`;
}
console.log(`${" ".repeat(h.level)}h${h.level} ${h.text}${note}`);
previous = h;
}
const top = list.filter((h) => h.level === 1).length;
console.log(` ${list.length} headings, ${top} at level 1, ${skips === 0 ? "no skipped levels" : `${skips} heading${skips === 1 ? "" : "s"} skip levels`}\n`);
} Output
balcony-garden.html
h1 Growing vegetables on a balcony
h2 Choosing containers
h3 Size
h3 Drainage
h2 Soil and compost
h2 What to grow first
h3 Leafy greens
h3 Tomatoes and chillies
h2 Watering in hot weather
9 headings, 1 at level 1, no skipped levels
balcony-garden-by-size.html
h1 Growing vegetables on a balcony
h3 Choosing containers <- skips h2: after an h1, the next heading goes no deeper than h2
h4 Size
h4 Drainage
h3 Soil and compost
h3 What to grow first
h4 Leafy greens
h4 Tomatoes and chillies
h3 Watering in hot weather
h6 Get one balcony tip a week <- skips h4 and h5: after an h3, the next heading goes no deeper than h4
10 headings, 1 at level 1, 2 headings skip levels
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node outline.mjs
The first outline reads like a table of contents: one title, four topics, and the parts of two of them. That is what a screen reader user gets too. Screen readers list the headings of a page and jump between them: NVDA, for example, moves to the next heading with the H key and to the next heading of a given level with the keys 1 to 6. Most screen reader users rely on it. In WebAIM’s tenth survey of screen reader users, 71.6 % of respondents said that navigating through the headings is the first thing they do to find information on a long page, and 88.8 % found the heading levels very or somewhat useful. The levels are information, not decoration.
That is also why WCAG 2.2 treats headings as structure. Its success criterion 1.3.1, Info and Relationships, asks that
structure shown visually, such as a heading in large bold text, can also be determined by software. A line of bold
text in a <p> looks like a heading but is missing from the outline.
What a skipped level looks like to a checker
The second version of the guide has the same content, but its author picked the levels by size: <h3> for the topics
because <h2> looked too big, and <h6> for a small sign-up box at the end:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Growing vegetables on a balcony</title>
</head>
<body>
<main>
<h1>Growing vegetables on a balcony</h1>
<p>A sunny balcony of two square metres can keep a small kitchen in greens for most of the year.</p>
<!-- The author wanted these headings smaller, so picked h3 and h4 for their size. -->
<h3>Choosing containers</h3>
<p>Almost anything that holds soil and drains water will do.</p>
<h4>Size</h4>
<p>Leafy greens need about 15 cm of soil; tomatoes need at least 30 cm.</p>
<h4>Drainage</h4>
<p>Drill holes in the bottom and raise the pot on two bricks.</p>
<h3>Soil and compost</h3>
<p>Mix garden soil, compost and coco peat in equal parts.</p>
<h3>What to grow first</h3>
<h4>Leafy greens</h4>
<p>Spinach and fenugreek are ready to cut in four to six weeks.</p>
<h4>Tomatoes and chillies</h4>
<p>Start them from seedlings and give them six hours of sun a day.</p>
<h3>Watering in hot weather</h3>
<p>Water early in the morning, and feel the soil before you water again.</p>
<!-- A sign-up box at the end, with the smallest heading of all. -->
<h6>Get one balcony tip a week</h6>
<p>Short, practical and never more than once a week.</p>
</main>
</body>
</html> The outline above shows the damage. “Choosing containers” now looks like a part of some missing section, and the sign-up box sits five levels below the page title. Three common checkers report the two jumps in their own words:
- The Nu Html Checker 26.10.2:
Error: The heading “h3” (with computed level 3) follows the heading “h1” (with computed level 1), skipping 1 heading level.For the<h6>, the same message endsskipping 2 heading levels. - html-validate 11.16.1, rule
heading-level:Heading level can only increase by one, expected <h2> but got <h3>, and the same for<h6>where<h4>was expected. - axe-core 4.13.0, rule
heading-order(moderate impact):Heading levels should only increase by one, on the same 2 elements, in Chromium 153 and Firefox 155.
All three were quiet about the first version. One detail matters if you set up html-validate yourself: its
heading-level rule runs only when its html-validate:document preset is switched on. With the recommended
preset alone, it reported nothing about either page.
The fix keeps the look and corrects the structure. Use <h2> for the four topics and <h3> for their parts, and if
the design wants the topic headings smaller, give them a class and set the size in CSS. Turn the sign-up box into its
own <h2>, or leave it without a heading if it is not a section of the guide.
Moving back up is always allowed. After an <h3> the next heading may be another <h3>, an <h2> or an <h1>. The
Standard’s rule only limits going down: less than, equal to, or one more than the level before.
What may follow an h2
Text description of the diagram
The diagram starts from a heading of level 2 and shows the four kinds of heading that could come next.
- An h1: allowed. Going back up starts a new top-level section.
- Another h2: allowed. It starts the next section at the same level.
- An h3: allowed. It names a part of the h2 section, one level deeper.
- An h4, h5 or h6: not allowed (drawn with dashed lines). It would be more than one level deeper than the h2 and skip at least one level.
The same rule holds from every level: the next heading may go back up to any level, stay at the same level, or go one level deeper.
An h1 inside a section is still level 1
For years, drafts of HTML described an “outline algorithm” in which an <h1> inside a <section> or an <article>
would count as a lower level, so a page could use <h1> everywhere. No browser or screen reader ever built it, and
the Standard later replaced it with the plain rule above; the change cited more than a decade in which no user agent
had implemented the algorithm. An <h1> is level 1 wherever it appears.
Browsers kept one leftover for longer: their default stylesheets drew an <h1> inside article, aside, nav or
section smaller, as if it were an <h2>. According to MDN’s browser-compat-data 8.1.4, Chrome 140, Firefox 140 and
Safari 26.2 removed those smaller sizes. This page nests an <h1> three levels deep:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>An h1 at four depths</title>
</head>
<body>
<h1>An h1 in the body</h1>
<section>
<h1>An h1 in a section</h1>
<article>
<h1>An h1 in an article inside the section</h1>
<aside>
<h1>An h1 in an aside inside both</h1>
</aside>
</article>
</section>
</body>
</html> In Chromium 153 and Firefox 155 all four were drawn at 32px with the same 21.44px margin above them, and Chromium’s
accessibility tree, the structure it gives screen readers, reported level 1 for each. So a page that relied on the old
smaller sizes now shows four headings of equal size, and its outline has four level-1 headings. Write the level you
mean: <h2> for a section inside an <h1> section, whatever elements wrap it.
Version note
The Standard has recently added two attributes for reused parts of a page: headingoffset shifts the levels of the
headings inside an element (an <h1> inside headingoffset="1" becomes level 2), and headingreset stops that
shift at a boundary such as a dialog. browser-compat-data 8.1.4 lists them only behind a flag in Chrome 151 and in a
Firefox preview build, and not in Safari; web-features does not list them as Baseline. In Chromium 153 and Firefox
155 without flags, an <h1> inside headingoffset="1" stayed level 1 at 32px. Until browsers ship them, write the
real levels.
Size is a job for CSS
Because every level has a default size, it is easy to choose <h4> because it looks right. Do the opposite: choose
the level from the outline, then make it look right with CSS. A topic heading stays an <h2> with a class such as
class="topic", and the stylesheet gives .topic the size the design asks for. Two things follow:
- The outline stays correct when the design changes, because only the stylesheet changes.
- Screen reader users hear the structure the page really has, not the one its fonts suggest.
The reverse mistake is just as common: text that is styled to look like a heading but marked up as a <div> or a <p>
with bold text. It looks the same, but it is missing from the outline, and nobody can jump to it.
Paragraphs, line breaks and thematic breaks
<p> is a paragraph. The Standard treats a paragraph as a structural block rather than a grammatical idea, and it gives
the <p> element a content model of phrasing content only: text and inline elements, but no lists, no headings and
no other blocks. The parser enforces that while it reads the page. This line asks for a list inside a paragraph:
<p>Pack these:<ul><li>Seeds</li><li>Soil</li></ul> and water them.</p>
Chromium 153 and Firefox 155 both built the same DOM from it: the <ul> start tag closed the paragraph, so the page
got <p>Pack these:</p>, then the list, then the text “and water them.” on its own, and then an empty <p></p>,
created by the </p> end tag that no longer had a paragraph to close. To keep a sentence and a list together, use
three blocks (a <p>, the list, another <p>) or wrap them in a <div>, which may contain both.
<br> is a line break that is part of the content, such as the lines of an address or a poem. The Standard says it
must not be used to separate thematic groups in a paragraph, and that a paragraph of nothing but one <br> must not be
used for presentation. <hr> is a thematic break between paragraphs: a change of topic or a scene change. Browsers draw
it as a line, and HTML-AAM maps it to the separator role. This fragment uses all three correctly:
<h2>Visit the community library</h2>
<p>The reading room is open to everyone, and you do not need a membership card to sit and read.</p>
<p>Borrowing books does need a card, which is issued the same day.</p>
<h3>Address</h3>
<p>
Community Library<br>
14 Station Road<br>
Pune 411001
</p>
<hr>
<p>Volunteers who can read aloud to children on weekends are always welcome.</p> And this is the common misuse, line breaks standing in for paragraphs and for spacing:
<!-- Line breaks used for layout: one "paragraph" that is really three, and empty lines for spacing. -->
<p>
The reading room is open to everyone.<br>
<br>
Borrowing books needs a card, issued the same day.<br>
<br>
<br>
Volunteers who can read aloud to children are always welcome.
</p> It looks similar on screen, but the page now has one paragraph where it means three, and the empty lines carry no
meaning at all. Use one <p> per paragraph and let CSS margins set the space between them.
Subtitles with hgroup
A subtitle or tagline is not a section of its own, so it should not be a heading. <hgroup> groups a heading with one
or more <p> elements that hold a subheading, an alternative title or a tagline. Its content model is zero or more
<p> elements, exactly one heading, then zero or more <p> elements:
<hgroup>
<h1>Growing vegetables on a balcony</h1>
<p>A beginner's guide for small, sunny spaces</p>
</hgroup>
<p>A sunny balcony of two square metres can keep a small kitchen in greens for most of the year.</p>
<h2>Choosing containers</h2> The outline still has a single entry for the title, “Growing vegetables on a balcony”, and the next section is an
<h2> as usual. <hgroup> creates no level of its own; it only says that the <p> belongs to the heading.
Try it: fix the outline of a help page
This help page has five problems in its headings. The site name in the header is marked up as a second <h1>, three
headings in the article skip a level, and the footer heading uses <h6> because it should look small, so it skips a
level as well:
<header>
<h1>City Library</h1>
</header>
<main>
<h1>How to get a library card</h1>
<p>A card lets you borrow up to four books at a time and use the computers.</p>
<h3>Who can apply</h3>
<p>Anyone who lives, works or studies in the city.</p>
<h3>What to bring</h3>
<h5>Proof of address</h5>
<p>A utility bill, a rent agreement or a college identity card.</p>
<h5>A photograph</h5>
<p>One passport-size photo, or we take one at the desk.</p>
<h2>Applying online</h2>
<p>Fill in the form, upload your documents and collect the card within a week.</p>
<h4>Paying the fee</h4>
<p>The yearly fee can be paid online or at the desk.</p>
</main>
<footer>
<h6>Questions? Call the front desk</h6>
<p>Open every day except public holidays.</p>
</footer> Rewrite it so that the page title is the only <h1>, no heading skips a level, and every heading names a real section.
Leave sizes to CSS. Check your version with the HTML Validator, or count the jumps yourself: axe-core reported
4 heading-order violations on this page and none on the answer. One answer:
<header>
<p class="site-name">City Library</p>
</header>
<main>
<h1>How to get a library card</h1>
<p>A card lets you borrow up to four books at a time and use the computers.</p>
<h2>Who can apply</h2>
<p>Anyone who lives, works or studies in the city.</p>
<h2>What to bring</h2>
<h3>Proof of address</h3>
<p>A utility bill, a rent agreement or a college identity card.</p>
<h3>A photograph</h3>
<p>One passport-size photo, or we take one at the desk.</p>
<h2>Applying online</h2>
<p>Fill in the form, upload your documents and collect the card within a week.</p>
<h3>Paying the fee</h3>
<p>The yearly fee can be paid online or at the desk.</p>
</main>
<footer>
<h2>Questions? Call the front desk</h2>
<p>Open every day except public holidays.</p>
</footer> Its outline:
- How to get a library card
- Who can apply
- What to bring
- Proof of address
- A photograph
- Applying online
- Paying the fee
- Questions? Call the front desk
The site name became a plain paragraph: it is the same on every page, so it does not name this page’s content. The
footer heading became an <h2>, because it starts a short section of its own after the article.
Check yourself
5 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.
Interview questions
Can a page have more than one <h1>?
Yes. The HTML Standard allows several headings of level 1, and its own examples include a conforming page with three
of them. What it requires is that, if a page has headings, at least one has level 1, and that no heading is more than
one level deeper than the one before it. Most pages are about a single subject, so one <h1> that matches the
<title> gives screen reader users and search engines the clearest start, and it keeps the outline simple. Several
<h1> elements are fine when a page really holds several top-level topics. One thing has changed: an <h1> inside a
<section> or <article> no longer looks or counts as a lower level. Browsers draw it at full size and expose it as
level 1, so nested parts need real <h2> or <h3> elements.
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Mid to senior
A product listing shows twenty cards, and each card's title is an h1, because the same card component is also used on the product's own page, where its title is the page heading. An audit flags the listing. What do you change, and how do you keep heading levels right wherever the component appears?
-
Mid to senior
An accessibility audit of a 2,000-page documentation site reports skipped heading levels on most pages, because writers pick h4 whenever they want a smaller heading. How do you find every case, fix them without changing the design, and stop them from coming back?
Key takeaways
<h1>to<h6>name sections; the number is the level, and all headings in order form the outline.- A heading may be at most one level deeper than the heading before it; going back up is always fine.
- Choose levels by structure and set sizes with CSS. Nu, html-validate (with its document preset) and axe-core all report skipped levels.
- An
<h1>is level 1 everywhere: nested ones are drawn at 32px and exposed as level 1.headingoffsetis not usable yet. <p>holds phrasing content only (a list ends it),<br>is for line breaks inside content such as addresses, and<hr>marks a change of topic.<hgroup>attaches a subtitle in a<p>to a heading without adding a level.
References
- HTML Standard: Headings and outlines (WHATWG)
- HTML Standard: the hgroup element (WHATWG)
- HTML Standard: the p element (WHATWG)
- HTML Standard: the hr element (WHATWG)
- HTML Standard: the br element (WHATWG)
- HTML Standard: Rendering, sections and headings (WHATWG)
- Replace the outline algorithm with one based on heading levels (HTML Standard pull request 7829) (WHATWG)
- MDN browser-compat-data: html.elements.h1 and the headingoffset attribute (Mozilla)
- Understanding WCAG 2.2: Info and Relationships (W3C)
- NVDA User Guide (browse mode and single letter navigation) (NV Access)
- Screen Reader User Survey 10 Results (WebAIM)
- html-validate rule: heading-level (html-validate)
- axe-core 4.13 rule: heading-order (Deque Systems)
- The Nu Html Checker (The Nu Html Checker project)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress