HTML Heading Levels and Sectioning Content

You may have noticed that there is no difference in size between an h1 heading and h2 heading within an article or section element. That’s weird. In this article, I’ll explain why it is, and the current recommendation for HTML heading levels.

Headings as Children of the body Element

First, let’s look at heading levels that are direct children of the body:

<body>
  <h1>Heading Level 1: No Nesting - at Root</h1>
  <h2>Heading Level 2</h2>
  <h3>Heading Level 3</h3>
  <h4>Heading Level 4</h4>
  <h5>Heading Level 5</h5>
  <h6>Heading Level 6</h6>
</body>

Result: Headings get consistently smaller from 32px to 10.72px (assuming a base font size of 16px): Headings as Children of the body Element


Headings as Children of a section Element. No Nesting.

Next, let’s take that same structure and throw it in a section element:

<body>
  <section>
    <h1>Heading Level 1: No Nesting - Decreasing Levels</h1>
    <h2>Heading Level 2</h2>
    <h3>Heading Level 3</h3>
    <h4>Heading Level 4</h4>
    <h5>Heading Level 5</h5>
    <h6>Heading Level 6</h6>
  </section>
</body>

Result: h1 and h2 are the same size (24px) and from there they descend in size to 10.72px (assuming a base font size of 16px): Headings as Children of a section Element. No Nesting.

The fact that the h1 and h2 are the same size is surprising and I believe is the result of an attempt to account for how web developers have acted in the past and how the standards bodies wished they would have acted.

Before I get into that though, let’s discuss structure for a moment. Consider the following code:

<body>
  <section>
    <h1>Fruit and Vegetables</h1>
    <p>Fruit and vegetables are good for you.</p>
    <h2>Fruit</h2>
    <ul>
      <li>Bananas</li>
      <li>Apples</li>
      <li>Grapes</li>
    </ul>
    <h2>Vegetables</h2>
    <ul>
      <li>Spinach</li>
      <li>Lettuce</li>
      <li>Zucchini</li>
    </ul>
    <p>Intelligence is knowing that a tomato is a fruit.
        Wisdom is not putting tomatoes in the fruit salad.</p>
  </section>
</body>

The lists are children of the sections implicitly created with the h2 elements that precede them. The last paragraph, however, is a child of the whole section, but not a child of the section denoted by the h2 element that precedes it. In other words, the witticism is not specific to the Fruit heading or the Vegetables heading. It has to do with both. To make this clear, we should add new sections:

<body>
  <section>
    <h1>Fruit and Vegetables</h1>
    <p>Fruit and vegetables are good for you.</p>
    <section>
      <h2>Fruit</h2>
      <ul>
        <li>Bananas</li>
        <li>Apples</li>
        <li>Grapes</li>
      </ul>
    </section>
    <section>
      <h2>Vegetables</h2>
      <ul>
        <li>Spinach</li>
        <li>Lettuce</li>
        <li>Zucchini</li>
      </ul>
    </section>
    <p>Intelligence is knowing that a tomato is a fruit.
        Wisdom is not putting tomatoes in the fruit salad.</p>
  </section>
</body>

This makes it really clear that the p element with the witticism is not a child of the Vegetablessection as we can show by applying this CSS rule:

section {
  outline: 1px dashed red;
  margin: .5em;
  padding: .5em;
}

The result:Styled Nested Sections.

One takeaway here is that authors should explicitly section elements and not rely on heading levels to implicitly create them. Indeed, this is recommended by WHATWG:

Authors are also encouraged to explicitly wrap sections in elements of sectioning content, instead of relying on the implicit sections generated by having multiple headings in one element of sectioning content.

Take another look at the styled screenshot above. I have not used CSS to change the size of the h1 and h2 elements. As you can see they are the same size. They are both 24px, which is the default size for an h2 element that is a child of the body, which implies that h1 and h2 elements in sectioned content are both considered to have the rank of h2 elements. Again, this is weird.

So, why this behavior? Again, I believe it is the result of an attempt to account for how web developers have acted in the past and how the standards bodies wished they would have acted.


How Web Developers Have Acted in the Past

Because older browsers didn’t support sectioning content, when web developers started using it, they commonly would use an h2 element for the top-level heading in a section, article, aside, etc. The purpose was to indicate that those headings were a rank below the headings that were direct children of the body.


How the W3C and WHATWG Wished Web Developers Had Acted

If the creators of the specifications could, I think they would like to magically change every top-level heading within sectioning content from an h2 to an h1 and then let the browsers use the nesting structure to decide on heading rank. For example, a body>section>h1 would be ranked lower than a body>h1.

In this scenario, the preferred structure would have the ranking decrease with every new section. Authors should then explicitly section elements and not rely on heading levels to implicitly create them. In this case, there is little need for h2 elements and lower:

<body>
  <section>
    <h1>Heading Level 1: Nesting - All h1 Headings</h1>
    <section>
      <h1>Heading Level 1</h1>
      <section>
        <h1>Heading Level 1</h1>
        <section>
          <h1>Heading Level 1</h1>
          <section>
            <h1>Heading Level 1</h1>
            <section>
              <h1>Heading Level 1</h1>
            </section>
          </section>
        </section>
      </section>
    </section>
  </section>
</body>

Modern browsers support this structure and will treat each h1 in a nested section as a lower rank than the h1 in the outer section. Here’s the output: nested headings - all h1s

Notice that the bottom two headings are the same size. That is because the h1 in the outermost section element is treated as a 2nd-level heading. As headings only go six levels, both headings in the fifth and the sixth nested level sections are treated as 6th-level headings.

Now let’s see what happens when we use lower-level headings for each nested section:

<body>
  <section>
    <h1>Heading Level 1: Nesting - Decreasing Levels</h1>
    <section>
      <h2>Heading Level 2</h2>
      <section>
        <h3>Heading Level 3</h3>
        <section>
          <h4>Heading Level 4</h4>
          <section>
            <h5>Heading Level 5</h5>
            <section>
              <h6>Heading Level 6</h6>
            </section>
          </section>
        </section>
      </section>
    </section>
  </section>
</body>

In this case, the outermost section’s h1 is still treated like a 2nd-level heading, but so is the second level’s h2 element! See below: nested headings - descending

This is super weird! To show just how weird, consider the result of this code:

<body>
  <section>
    <section>
      <h1>Heading Level 1</h1>
      <h2>Heading Level 2</h2>
    </section>
  </section>
</body>

The result:h1 smaller than h2

A section>section>h2 is treated as a 2nd-level heading while a section>section>h1 is treated as a 3rd-level heading!

While this seems completely nuts to me, it is the recommended approach of WHATWG, except that they recommend you begin with an h2 element as the top-level heading of sectioning content. Here’s the example they give:

<body>
  <h1>Apples</h1>
  <p>Apples are fruit.</p>
  <section>
    <h2>Taste</h2>
    <p>They taste lovely.</p>
    <section>
      <h3>Sweet</h3>
      <p>Red apples are sweeter than green ones.</p>
    </section>
    <section>
      <h3>Color</h3>
      <p>Apples come in various colors.</p>
    </section>
  </section>
</body>

Two things I don’t like about this:

  1. If I decide to add another level of nesting in the middle of the tree somewhere, I need to change the heading levels of the lower nested sections.
  2. If the content is syndicated in some way and inserted in someone else’s tree, it will get messed up. Consider the following:
    <body>
      <section>
        <h2>Heading Level 2</h2>
        <section>
          <h3>Heading Level 3</h3>
          <p>The content below is pulled from somewhere else:</p>
          <article>
            <!-- Syndicated content -->
            <h2>Heading Level 2</h2>
            <section>
              <h3>Heading Level 3</h3>
            </section>
          </article>
        </section>
      </section>
    </body>
    This will output: sections with pulled content Using all h1 elements for headings would fix this:
    <body>
      <section>
        <h1>Heading Level 1</h1>
        <section>
          <h1>Heading Level 1</h1>
          <p>The content below is pulled from somewhere else:</p>
          <article>
            <!-- Syndicated content -->
            <h1>Heading Level 1</h1>
            <section>
              <h1>Heading Level 1</h1>
            </section>
          </article>
        </section>
      </section>
    </body>
    This will output: sections with pulled content h1s

My Recommendation?

My recommendation is to go with the recommended approach in the specification, because who am I to tell you to do it differently. Then use a normalizer like normalize.css, which does not base the size of the heading level elements on their location in the document tree. Then use your own CSS to set the sizes you want.

Written by Nat Dunn. Follow Nat on Twitter.


Related Articles

  1. HTML Heading Levels and Sectioning Content (this article)
  2. How to Ask Good Technical Questions
  3. How to Install and Use Visual Studio Code for Class
  4. How to Force a Refresh of favicon.ico