Some accessibility improvements are invisible to the sighted eye. One of my favorite lessons learned last year was all about invisible headings in HTML landmarks.

In this article, I will show you how to implement these headings and then explain their benefit.

I already touched upon this subject in my article Blog improvements: headings and settings. In this new article, I want to elaborate further on the topic.

At the bottom of the article are references to more in-depth articles from other authors, which I highly recommend checking out!

What are landmarks?

Before we get started, please familiarize yourself with HTML landmarks:
Wikipedia: HTML landmarks.

HowTo

Let's get our hands dirty and improve an HTML example.

Example HTML

Imagine this basic HTML layout: It has a site-wide header, primary navigation, and footer. The main content depends on the website's current page (or document).

The HTML would look something like:

<div class="page-outline">
    <header>
        <a href="/">
            <img src="awesome-logo.svg" alt="Some Company logo: Visual description of logo."><!--
        --></a>
    </header>

    <nav>
        <ul>
            <li><a aria-current="page" href="/webshop">Buy our stuff</a></li>
            <li><a href="/about">About</a></li>
            <li><a href="/support">Support</a></li>
        </ul>
    </nav>

    <main>
        <h1 class="page-title">Buy our stuff</h1>
        <p>Lorem ipsum yada yada yada.</p>
    </main>

    <footer>
        <p><small>Some copyright notice you probably don't need.</small></p>
    </footer>
</div>

Choosing landmarks to add headings to

Aside from the main element, there are three landmarks in the example HTML:

  1. Page header;
  2. Primary navigation;
  3. Page footer.

We want to add headings to the regions that are important for users to have easy access to. Does that mean we will add a heading to each of the three landmarks above? Spoiler alert: no, it does not.

The primary navigation is an obvious candidate for a heading because we use it all the time to navigate the website. You could also have more than one navigation element on a page, so it is helpful to distinguish between each.

The page footer typically contains helpful links and information and is often (implicitly) treated as secondary navigation, so that is another candidate.

The header, however, is the first thing on the page, and there are keystrokes that jump to the start of the document, so it doesn't need a heading.

Adding headings

Let's add the headings following this strategy:

  • Use an H2 (<h2>) element;
  • Add a class visually-hidden to each heading (more on this later);
  • Semi-optionally add a unique ID to each heading (more on this later).

The primary navigation becomes:

<nav>
    <h2 id="primary-navigation-heading" class="visually-hidden">Primary navigation</h2>
    <ul><!-- List items --></ul>
</nav>

And the page footer becomes:

<footer>
    <h2 id="footer-heading" class="visually-hidden">Footer</h2>
    <p><small>Some copyright notice you probably don't need.</small></p>
</footer>

The headings are still visible; we will fix that later with CSS.

HTML-wise, this is all you need to do to add headings. However, the primary navigation can still benefit from one more change, which I will show you next.

Labeling the primary navigation

HTML navigation landmarks are not strictly unique, unlike the page header, footer, and main content.

Imagine our layout does not have one navigation element but two: a primary and a secondary navigation menu.

In the heading outline, you can easily distinguish which navigation is which simply by reading the heading contents. However, the landmark outline will call both "navigation" as they are anonymous (unlabeled) elements.

Luckily, one attribute allows us to use one HTML element to label another: aria-labelledby. All this attribute needs is the ID of the element that will act as the label.

Luckily, we already added a heading ID in the previous section, so let's add the attribute now:

<nav aria-labelledby="primary-navigation-heading">
    <h2 id="primary-navigation-heading" class="visually-hidden">Primary navigation</h2>
    <ul><!-- List items --></ul>
</nav>

Sadly, this solution has one drawback: screen readers and other outline tools may announce the landmark role after its label. This means our <nav> element could be announced as "Primary navigation navigation".

Ideally, we would use a heading with the text "Primary navigation" and replace aria-labelledby on the <nav> element with aria-label="Primary" to avoid word redundancy. However, we should not use aria-label due to accessibility concerns; more on that later.

So, for now, we will accept the potential double announcement of the word "navigation".

Visually hiding the headings

Now, we need to hide the headings from view using CSS.

There are several ways to hide elements in HTML and CSS, the most popular of which is probably display: none;. In our situation, this is precisely the method we want to avoid.

When you remove an element's display, it will no longer be rendered, removing its visibility for screen readers and other software. It would effectively be as if there are no headings at all (which would make this entire article rather pointless, don't you think?)

Luckily, there is a CSS utility for hiding elements from sight only. I will not explain how it works but will instead leave some useful links at the bottom of the article.

Register this CSS in your stylesheet:

.visually-hidden:not(:focus):not(:active) {
    clip: rect(0 0 0 0); 
    clip-path: inset(50%);
    height: 1px;
    overflow: hidden;
    position: absolute;
    white-space: nowrap; 
    width: 1px;
}

I use a slightly different version of this selector in my personal projects, but the original works in more browsers (mine does not support Internet Explorer).

In case you are interested, this is my tweaked version of the original with logical properties, a combined :not() pseudo-class, and without clip:

.visually-hidden:not(:focus, :active) {
    block-size: 1px;
    clip-path: inset(50%);
    inline-size: 1px;
    overflow: hidden;
    position: absolute;
    white-space: nowrap;
}

The solution with visually hidden headings and aria-labelledby is neat because it improves our code in multiple ways.

For one, the new headings give navigational tools extra handholds. Take screen readers: users have a shortcut to critical areas like the primary navigation or the page footer when navigating between headings.

Secondly, aria-labelledby is more beneficial than aria-label due to an issue with content translation services: attributes like aria-label and aria-description may not get translated by automatic translation tools—however, the heading content will.

So, using a heading, we improve site navigation and automatic translation. I call that a win-win situation!

Out-of-scope note: Please slap a (correct) lang attribute on the <html> element, or tools may still wrongly interpret page contents!

Disclaimer: Use headings and labels with constraint

You may now be tempted to add headings and aria-labelledby all over your pages, but please do not.

Screen readers announce headings and landmarks in order of occurrence without skipping, and useless headings contribute to cognitive overload.

Deliberate which regions warrant extra attention and act accordingly.

Somewhat related: An unlabeled <section> behaves no differently from a <div>; it only becomes a landmark if you label it. In prose, it is usually sufficient only to use headings and omit aria-labelledby on sections.

Wait, I thought we weren't supposed to skip heading levels?

If you were anything like me when you learned HTML, you were drilled with the phrase, "Thou shalt not skip heading levels."

This phrase refers to the fact that a H4 should only follow an H3, not an H2. To visualize this, think of collected HTML headings as a table of contents. A table of contents does not skip levels, but nests sections one level deeper at a time.

However, this rule is too rigid in a page-wide context, and this is due to H1 usage rules, in particular:

  • A page or document should contain only one H1;
  • The H1 represents the page or document title.
  • The H1 should be the first content in the HTML <main> element.

Considering these rules, you realize an H1 should not be used in other landmarks.

Page landmarks, like the top header, footer, and navigation, follow the main content in terms of importance: they allow users to navigate through a website. Regardless of content, we also want them to use the same heading levels on each page consistently. Considering this, using an H2 makes sense here.

Remember that the original rule of not skipping levels applies to each landmark: if your primary navigation starts with an H2, its next nested heading may not be an H4.

In short: You should not skip heading levels within landmarks. Opening secondary, site-wide landmarks with an H2 is acceptable, as the H1 is reserved for the primary content.

Wrap-up

Use visually hidden headings to add extra points of navigation to your pages. Optionally, use the headings as labels for non-unique landmarks.

Just be careful not to overdo it on the headings and labels, or you will cause cognitive overload.

Hat tip to James Scholes for teaching me this technique and proofreading this article!

See also

Music tip

Listen to "Give Us The Moon" by The Night Flight Orchestra.