Where Should Page Titles Live?

Martin Underhill makes a case that the main page <h1> doesn't belong inside a <header> element, whether that header is part of the <main> content or sits outside it. The reasoning comes down to how those structures are exposed to assistive technology.

Three common placements for the <h1> are:

  • In the site-wide <header>, alongside the site title
  • In a <header> nested within <main>
  • In a dedicated <header> positioned outside of <main>

The first option is questionable because the site title serves a different role than the page title. The other two structures create more subtle issues with semantics and navigation. A <header> is meant to wrap introductory content, and it may contain a heading element — but a heading is arguably part of the main content rather than something separate from it.

<!-- 1️⃣ -->
<header>
  <!-- Header stuff -->
  <h1>Page heading</h1>
</header>
<main>
  <!-- Main page content -->
</main>

<!-- 2️⃣ -->
<main>
  <header>
    <!-- Header stuff -->
    <h1>Page heading</h1>
  </header>
  <!-- Main page content -->
</main>

Consider the implications:

  • Heading in a <header> outside of <main>: A user jumping to main content via a skip link might completely miss the heading. A screen reader user navigating by landmark could also miss it, or get verbose announcements about the end of the banner before reaching the main content.
  • Heading in a <header> nested inside <main>: The nested <header> loses its landmark semantics, effectively behaving like a generic <div> or <section>. That muddies where the main page header landmark is for screen reader users.

Underhill's recommended structure is to place the heading directly inside <main>, with no <header> wrapper around it:

<!-- 3️⃣ -->
<header>
  <!-- Header stuff -->
</header>
<main>
  <h1>Page heading</h1>
  <!-- Main page content -->
</main>

This approach keeps the <header> landmark (and its role) intact, connects the <h1> to the main content, and makes navigation between header and main predictable. As he notes, it's a minor point, but one that matters when you think beyond the visually obvious.

A More Rigorous Approach to Fluid Type

Responsive typography with clamp() is well documented, but Donnie D'Amato points out that most tutorials skip over how to pick the values. The common pattern sets a minimum and maximum font size, then uses a viewport unit for the middle value so text scales fluidly between them.

.article-heading {
  font-size: clamp(<min>, <ideal>, <max>);
}

Typically, the minimum and maximum come from your actual font scale:

.article-heading {
  font-size: clamp(18px, <ideal>, 36px);
}

And the preferred middle value is expressed in viewport units for the fluid scaling:

.article-heading {
  font-size: clamp(18px, 4vw, 36px);
}

That approach has an accessibility catch: when a user zooms the page, font sizes set with viewport-based units behave differently from those set in absolute units. Maxwell Barvian's suggestion, covered on Smashing Magazine, is to make the middle calculation reference a non-viewport unit so it respects user zoom settings.

D'Amato's proposed formula computes the middle value from the difference between the minimum and maximum sizes, relative to the supported range of characters per line (roughly 40 to 80) and the smallest viewport you want to support (usually 320px), with everything converted and combined in rem units:

.article-heading {
  --heading-smallest: 2.5rem;
  --heading-largest: 5rem;
  --m: calc(
     (var(--heading-largest) - var(--heading-smallest))
     / (30 - 20) /* 30rem - 20rem */
   );
  font-size: clamp(
    var(--heading-smallest),
    var(--m) * 100vw,
    var(--heading-largest)
  );
}

In practice, this calculation proved unreliable. It works in Chrome and Safari when you substitute unit-less values with rem, but Firefox doesn't accept division of units by other units — which aligns with the specification.

A CSS Pseudo-Class for Headings

Firefox Nightly recently picked up a new addition to CSS: the :heading pseudo-class and the :heading() function. Alvaro Montoro explains that these let you target heading elements far more cleanly than relying on element selectors.

  • :heading: Matches any <h*> element.
  • :heading(): Matches a specific subset of headings, e.g. :heading(h2, h3).

Instead of writing a long group of selectors for every heading level:

h1, h2, h3, h4, h5, h6 { }

You can condense it to:

:heading { }

Likewise, for a subset of heading levels, the older form:

h2, h3 { }

Becomes:

:heading(2, 3) {}

Montoro also notes these pseudo-classes are strictly scoped to actual heading elements — they won't match elements that merely look like headings via HTML attributes or ARIA roles. That distinction is worth reading about in full. As of now, the feature is only in Firefox Nightly.