heading
Web Development |

Heading for Greatness — A React Developer's Guide to HTML Headings

Moritz Jacobs

January 29, 2024

tl;dr quick summary
Although it may seem trivial, using HTML headings correctly is neither easy nor unimportant. This article covers best practices and common pitfalls, shows you how to create well-structured, accessible, and SEO-friendly headings, and introduces a handy React package for managing them.

Introduction ☝️

The HTML headings h1 through h6 are among the most basic building blocks for structuring content in a browser. They have been around since all the way back in 1991, when Tim Berners-Lee added them to his first draft of "HTML". A few years later, Berners-Lee and his colleague Dan Connolly wrote the first proposal for an official HTML specification. It already mentions a practice that every web developer should know but that is too often not followed:

It is not normal practice to jump from one header to a header level more than one below, for example for follow an H1 with an H3. Although this is legal, it is discouraged, as it may produce strange results for example when generating other representations from the HTML.

If you stop reading here, take this message home with you: Use exactly one <h1> per page and do not skip heading levels!

<h1>This is the page's main heading ✅</h1>
<h2>This is also fine ✅</h2>
<h4>This should have been preceded by an h3! ❌</h4>

Why heading order matters

Typically these headings are styled with a descending typographic emphasis: h1 is large and bold (very prominent), h2 a little less and so on. If your content is like this article or a scientific text, the reason for this is obvious: it helps the reader to understand the hierarchical structure of your content.

h1, h2, h3, h4, h5, h6 {
  font-weight: bold;
}

h1 { font-size: 3.75rem };
h2 { font-size: 3rem };
h3 { font-size: 2.125rem };

...

The second reason is machine readability: properly set headings describe the outline of your document in a way that is suitable for …

  • Assistive technology (e.g. screen readers for visually impaired users) and
  • Crawlers such as the Google bot. Fortunately, good accessibility practices and good SEO often go hand in hand 💪
  • Automatic Table Of Contents generation
  • Use in popular testing frameworks, for example page.getByRole("heading", ...) in Playwright

In such documents, the DOM element used (e.g. <h2>) and the styling applied to it (e.g., bold, large text, perhaps in a different color) are tightly coupled. The outline of the document manifests itself both on the semantic level (the DOM, the "hidden outline" so to speak) and on the visual level (what you can actually see on the screen).

So what is the problem? 😱

Problems tend to occur on pages that are not purely text driven. For example, marketing pages, e-commerce pages, landing pages, … These pages are typically design-oriented with just enough text to sell something. They need to be aesthetically pleasing and concise. As a result, they're often not easily consumable by screen readers, for example.

Most of the web is made to appeal to sighted users, who are rarely affected by the semantics of HTML. If we as web developers don't actively try to keep assistive technology users in mind when implementing a design, no one else will. This should concern designers as well!

There are several types of problems, and each has a solution:

#1: »It looks like a heading but it's not semantically suited to be one! 😖«

This is the most common mistake devs make: a typographic choice does not automatically translate to a heading level. Just because some text on a page is emphasized that way, it's not necessarily a particular type of heading. In other words: don't reach for that <h2> just because this text is big and bold!

When working on the look and feel of a project, designers often come up with typographic styles and scales. These are often also named "h1", "h2" and so on — in my personal opinion, this is a mistake. We attach too much semantic meaning to the name of a particular style. I would rather find more abstract names for these styles, like "text-xl".

Implementation-wise, that would mean that you should use a CSS reset to level the playing field and then only apply styling based on classes, not on element names:

.text-xl {
  font-size: 2rem;
  line-height: 1.5;
  color: var(--primary);
  font-weight: bold;
}
<h2>This heading will look…</h2>
<p>… just like this paragraph, because of the CSS reset.</p>

<p class="text-xl">And this paragraph will look…</p>
<h2 class="text-xl">… just like this heading, because of the class</h2>

Most UI libraries that provide typography components do this as well:

/* chakra-ui */
<Heading as="h2" size="xl">Lorem ipsum</Heading>

/* MUI — note that "h1" is also a style 🤦 */
/* cf. https://mui.com/material-ui/react-typography/#usage */
<Typography variant="h1" component="h2">Lorem ipsum</Typography>

#2: »The design doesn't want a heading here, but the structure needs one 🥺«

Not a problem, you can always add an invisible heading:

<h2 class="sr-only">This will be in the DOM but not visually present</h2>
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border-width: 0;
}

Again, the UI libraries know this too:

/* chakra-ui */
<VisuallyHidden>
  <h2>Lorem ipsum</h2>
</VisuallyHidden>;

/* MUI */
import { visuallyHidden } from "@mui/utils";
<Typography sx={visuallyHidden} component="h2">
  Lorem ipsum
</Typography>;

#3 »I'm not sure which heading level to use in my component 😐«

This is a tricky one. When you decide which heading level to use within a component, you are always restricting the component to a particular context within the document outline. But components are often used in multiple places and would require a more dynamic handling of their heading levels. Like this example:

type MyTextComponentProps = {
  as: "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "p";
  children: React.ReactNode;
};

const MyTextComponent = ({ as: As, children }: MyTextComponentProps) => (
  <div className={myBaseTextStyles}>
    <As>{children}</As>
  </div>
);

const App = () => (
  <main>
    <MyTextComponent as="h1">Lorem ipsum</MyTextComponent>
    <MyTextComponent as="h2"></MyTextComponent>
    <MyTextComponent as="p"></MyTextComponent>
  </main>
);

This works, but it feels a bit clumsy.

Tenon-UI had this great concept of heading level boundaries where you don't have to worry about using actual levels 1 through 6, but instead structure your component tree into semantic sections. Since this concept is baked into Tenon-UI (which seems to be dead) we decided to make this idea usable in any React-based application with our own package:

Meet uberschrift 🧢

uberschrift (named after the German word for "headline") gives you two components: one we call <Hx> (as in "heading level x"), which you can use for your headings without even thinking about context and level; and a <HxBoundary>, which you use to structure your document. Within these boundaries, <Hx> always chooses the correct heading level. If you need to structure your document one level deeper, you wrap your subcomponents in a new <HxBoundary>. The setup is straight forward:

npm install uberschrift --save
import { Hx, HxBoundary } from "uberschrift";

<Hx>I'm the h1!'</Hx>

<HxBoundary>
    <Hx>I want to be an h2</Hx>

    <HxBoundary>
        <Hx>I will be an h3</Hx>
    </HxBoundary>
</HxBoundary>

… will magically render like this 🪄

<h1>I'm the h1!'</h1>
<h2>I want to be an h2</h2>
<h3>I will be an h3</h3>

It is entirely powered by React Context, ready for React Server Components, zero dependencies, under 1kb and tree-shakeable! It's also usable with most popular UI libraries, check our documentation page for an explanation or take a look at our examples.

npm version badge Issue badge CI badge

Conclusion

When it comes to web development, as with all programming, the devil is in the details. There are many different requirements for the implementation of the simplest things: SEO, accessibility, code quality, ... The ever-increasing complexity of the web is embedded in even its smallest elements. Even headings.

We hope this article and package will help you manage this complexity a little better.

HTML Headings

Best Practices

Uberschrift

Accessibility

SEO

Peerigon logo