← Back to writing

ACCESSIBILITY

Headings Lead the Way

How heading structure helps people navigate a page, understand relationships, and reduce the work of making sense of it.

A page can look perfectly organized and still have almost no useful structure in the HTML.

A designer can make the page title large and bold. Section titles can be smaller but still visually distinct. A dialog can have a prominent title at the top. A side panel can look like a self-contained part of the interface.

Sighted users can often infer the hierarchy from font size, weight, spacing, borders, and position. None of those things make text a heading.

If the implementation uses a collection of styled divs, the visual hierarchy may be obvious while the semantic hierarchy is nearly flat.

For someone navigating with a screen reader, headings can be the way through the page.

The visual hierarchy may be obvious to one person while the navigable structure is almost missing for another.

The structure behind the screen

HTML gives us six heading levels: h1 through h6. Those levels communicate relationships between sections of content.

A simple page might look like this:

<h1>Account settings</h1>

<h2>Profile</h2>
...

<h2>Security</h2>

<h3>Password</h3>
...

<h2>Notifications</h2>
...

The heading levels tell us something about the content even without the visual design. Profile, Security, and Notifications are peers. Password belongs inside Security.

That hierarchy is useful to browsers and assistive technologies. W3C describes headings as a way to communicate how content is organized and to support in-page navigation.

The CSS can still make any of those headings look however the design requires. A heading level should not be chosen because the browser's default h3 styling happens to look close to the mockup.

Use CSS for presentation; choose the heading level from the structure.

What a screen reader gets

One of the easiest ways to understand this is to stop looking at the page and look at its heading list instead.

VoiceOver on macOS has a rotor that can expose headings on a webpage as a navigable list. A user can move through those headings, jump directly to one, and even narrow the list to a specific heading level.

Imagine opening the rotor and hearing something like this:

Heading level 1: Settings
Heading level 3: Profile
Heading level 4: Password
Heading level 2: Notifications
Heading level 4: Email
Heading level 1: Delete account

The page might still look polished. The heading list tells a different story.

The jumps make the relationships harder to reconstruct: Profile moves from level 1 to level 3, Password drops another level, Notifications returns to level 2, and Delete account becomes another page-level heading.

Now compare it with this:

Heading level 1: Settings
Heading level 2: Profile
Heading level 3: Password
Heading level 2: Notifications
Heading level 3: Email
Heading level 2: Delete account

Without seeing the layout, you can reconstruct quite a bit of the page.

I use the heading list as a quick test: if it is hard to understand there, the page structure probably needs another look.

The rotor is also a good reminder that headings are not just something a screen reader announces while reading from top to bottom. They can be used as shortcuts. A person may never read the page in the order the visual design expects. They may open the headings list and jump directly to the section they need.

When visually prominent text is not implemented as a heading, that shortcut disappears.

A modal still needs structure

Dialogs are a place where heading semantics often get lost.

A design may contain a large title such as “Delete project?” followed by explanatory text and a set of actions. The title looks like a heading, but the implementation may be nothing more than a styled div:

<div class="dialog-title">Delete project?</div>

That gives us the visual treatment without the semantic structure.

A better implementation might be:

<dialog aria-labelledby="delete-project-title">
  <h2 id="delete-project-title">Delete project?</h2>
  <p>This action cannot be undone.</p>
  ...
</dialog>

Now the visible title participates in the structure instead of only looking like one.

It is a heading inside the dialog, so someone navigating by headings can find it. It also provides the dialog's accessible name through aria-labelledby.

The WAI-ARIA Authoring Practices Guide recommends that a dialog have an accessible name, typically by referencing a visible dialog title with aria-labelledby when one exists.

The correct heading level depends on where the dialog sits in the broader document structure. The fact that it is a dialog does not automatically make its title an h1.

Component libraries make this harder. A Dialog knows that it needs a title, but it usually cannot know which heading level makes sense everywhere it will be used.

Panels and drawers have the same problem

Side panels, drawers, inspector panels, filter panels, and similar UI often feel like small pages inside the page. They usually have a title, content, controls, and sometimes their own subsections.

The visible title should generally be part of the heading hierarchy when it introduces a real section of content.

For example:

<h1>Search results</h1>

<h2>Filters</h2>
...

<h2>Results</h2>
...

If Filters happen to appear inside a slide-out panel, that does not erase its relationship to the page.

What I would avoid is baking a fixed heading level into the component simply because it looked correct in the first implementation:

function PanelTitle({ children }) {
  return <h2>{children}</h2>
}

That may work on one page and be wrong on another.

Sometimes the better component API lets the consuming page determine the level. Sometimes the heading is composed outside the component. The exact API can vary, but the hierarchy should come from the content structure rather than the visual shell around it.

Regions should have useful names

Headings can also help label regions of a page.

Consider a section containing recent activity:

<section aria-labelledby="recent-activity-heading">
  <h2 id="recent-activity-heading">Recent activity</h2>
  ...
</section>

The visible heading introduces the section and also supplies its accessible name.

W3C's landmark guidance recommends using aria-labelledby when an area already begins with a visible heading that can identify the region.

The same visible heading can carry the section structure and the region name, so there is no need to bolt on a separate invisible label later.

There is also a limit here. Not every visual container needs to become a region, and not every card needs a heading. Too many landmarks and headings can make navigation noisier rather than more useful.

Add structure because it helps people understand or navigate the content, not because a container happens to exist.

Components should not assume they own the page

Using one h1 for the page title is still a useful default. It gives the document a clear top-level subject, with the major sections organized underneath it.

Problems show up when individual components make that decision for themselves.

A PageTitle might render an h1. Then a dialog title renders another h1. A card uses one because its title is visually prominent. A side panel does the same because, inside that component, the title feels like the most important piece of text.

Each choice can look reasonable in isolation. Together, they can produce a heading list that no longer describes the structure of the page very well.

Component-based applications make this easy to miss. A component knows what it contains, but it does not necessarily know where it will be used.

A dialog knows that it needs a title. It does not know that its title should be the page's h1.

A panel knows that it introduces a group of content. It does not know whether that content sits beneath an h1, an h2, or another section entirely.

The page using the component has the context needed to make that choice.

When I review these patterns, I generally want one clear h1 identifying the page and heading levels beneath it that reflect the actual content relationships. If a reusable component renders a heading, its API or composition model should leave enough room for the consuming page to choose the appropriate level.

The component stays reusable without quietly rewriting the document hierarchy.

Do not choose headings for their size

I see this mistake a lot because it can happen without anyone thinking about accessibility at all.

Someone wants a piece of text to look smaller, so they change:

<h2>Billing information</h2>

to:

<h4>Billing information</h4>

It may match the design, but it now misrepresents the document structure.

Heading levels are not a typography scale.

If the content is an h2 structurally, keep it an h2 and style it to match the design.

The reverse mistake happens too. Large or bold text is not automatically a heading.

A dashboard might show:

$4,291
Account balance

The amount may be the largest thing in the card, but that does not make it a heading. Visual emphasis and document structure answer different questions.

Avoid skipping levels when opening sections

Heading levels should generally progress in a logical order.

If an h2 contains a subsection, that subsection should normally begin with an h3 rather than jumping straight to h4 or h5.

For example:

<h1>Settings</h1>
<h2>Security</h2>
<h4>Password</h4>

The Password heading visually might look fine, but the markup suggests a missing level in the hierarchy.

W3C recommends avoiding skipped ranks where possible when opening subsections. Moving back to a higher level when a subsection ends is different. An h2 can follow an h4 if the h4 was part of the previous h3 section and the document is now returning to a new top-level section beneath the h1.

The sequence is about relationships, not simple arithmetic.

Do not fake headings with styled text

This one is easy to miss in modern component code:

<div class="heading-xl">Payment details</div>

The class name tells the developer exactly what the text is supposed to look like. It tells the browser almost nothing about what the text means.

If Payment details introduces a section, use a heading element and put the visual style on that element.

The same applies when a design-system typography component defaults to div or span. A component named Heading should not merely reproduce a heading font style. Its rendered semantics need to be deliberate too.

Do not add headings everywhere

There is an opposite failure mode where a team learns that headings help navigation and starts turning every prominent label into one.

The result can be a heading list that is harder to navigate than the page it was meant to clarify.

A button label is not a heading. A form label is not a heading. A metric is not automatically a heading. A card title may or may not be one depending on whether the card represents a meaningful section of the surrounding content.

Heading navigation works because the list is a useful summary of the page. If every small visual group becomes a heading, that summary becomes cluttered.

I find it more useful to ask whether the text introduces a section someone might reasonably want to navigate to.

Empty headings are worse than they look

I know you probably wouldn't do this, but this section is here because I've seen it in the wild. Spacing hacks occasionally leave behind markup such as:

<h3>&nbsp;</h3>

or a component that renders a heading even when its title prop is empty.

There may be almost nothing visible on screen, but the empty heading can still appear in the semantic structure and create a strange stop for assistive technology.

Use CSS for spacing, and do not render the heading when there is no heading content.

Heading semantics in a design system

Reusable UI makes heading structure more complicated because a component usually knows what it is, but not where it will appear.

A Card can know that it has a title. It cannot always know whether that title should be an h2, h3, or plain text.

A Dialog can know that it needs an accessible name. It cannot safely assume that its visible title is always the page's h1.

A Panel can know that its content is grouped. It may not know whether that group deserves a heading in every context.

A component API should make that structural choice visible instead of burying it.

A design system can document when a title should be a heading, allow the consumer to provide the correct level, and test the rendered element instead of only testing how the text looks.

For example, an API might allow:

<Card title="Recent activity" headingLevel={2} />

Another system might prefer composition:

<Card>
  <h2>Recent activity</h2>
  ...
</Card>

Neither pattern is automatically better everywhere. The important part is that the component does not hide the structural decision from the person who has enough context to make it.

Newer platform features such as headingoffset are also starting to address the problem of reusable components participating in a larger heading hierarchy. They are worth watching, but they do not remove the need to understand the structure you are trying to create.

How I review a page

When I review heading semantics, I try to separate the visual design from the structure for a moment.

I look at the actual h1 through h6 elements. I check whether the page has a clear h1. I look for jumps that do not make sense. I pay attention to titles inside dialogs, panels, and meaningful regions. Then I inspect the heading list with assistive technology instead of relying only on the DOM.

The VoiceOver rotor is especially useful here because it turns the page into the navigation structure a user may actually encounter.

If I see:

Settings
Profile
Password
Notifications
Email
Delete account

and the levels describe the relationships correctly, I can understand the page without depending on the typography.

If half of the important section titles are missing, or the outline jumps around because components chose heading levels independently, the visual polish is hiding a structural problem.

Headings are a small part of HTML, but they carry a lot of information. They tell people where they are, what belongs together, and where they can jump next.

A heading should look right in the design. It should also still make sense when the design is gone.

References

Keep exploring

More notes on accessible product engineering, design systems, frontend quality, and practical AI-assisted delivery.

View all writing