Level-Setting Heading Levels

TL;DR: Avoid setting heading levels greater than six (6). This applies whether using aria-level or the proposed headingstart HTML attribute. Use HTML <h#> elements whenever possible.

ARIA

The aria-level attribute, when applied to headings (or nodes with the heading role) lets authors set an integer value for a heading level. While aria-level has been in the ARIA spec since 1.0, it had this note added in the ARIA 1.3 draft:

On elements with role heading, values for aria-level above 6 can create difficulties for users. Also, at the time of this writing, most combinations of user agents and assistive technologies only support aria-level integers 1-9 on headings.

This note is the outcome of the ARIA Working Group declining to impose a 1–6 integer limit on aria-level in February 2023. Instead it was based on support at the time.

HTML

HTML has always had headings, specifically levels 1 through 6.

Mosaic web browser, Options menu expanded that allows the user to choose the font for H7.
NCSA Mosaic had a “Header 7” option.

While HTML5 tried to make good on an idea originally in the first version of HTML, specifically a document outline algorithm, it proved to be untenable and no browser ever implemented it. In 2016 I wrote about the effort to get it removed from the HTML specification, considering the negative the impact it had on users.

One outcome of that effort, or many outcomes that all failed to get traction until now, is the concept of a heading level offset. This would allow authors to integrate content that already has a heading structure, while dynamically adjusting all those headings based on a cue from the containing document.

The headingstart attribute as proposed in 2019 (though it went through a few names) is the one getting traction. This shifts the burden from authors to browsers, which is better than users but which still will require work by user’s browser on every load, versus the author’s templates on a single render.

Browsers

Browsers will need to support this new HTML property for it to stand a chance. We can look to current support for aria-level for insight how well this might work.

Gecko (Firefox) and WebKit (Safari) both support values over two billion, or 2,147,483,647 (simplified as 231−1). Chromium (Chrome, Edge) supports only up to level nine (9). Chromium fixed support for aria-level above 6, but intentionally not for headings (I’ll get to why).

Meanwhile, Chromium has logged its intent to prototype support for headingstart. The Chrome Platform Status page goes into more detail. There is no indication it will or won’t support heading levels past 9.

When Chromium encounters a value above 9 or an invalid value, it falls back to a value of 6. Gecko will fall back to 2 for invalid values, unless the value was set on a <h#> element, in which case it will use the element’s level. WebKit will also fall back to the <h#> element, but otherwise is fine assigning no level for invalid values — with the exception of aria-level="-1", exposed as 4294967295.

AAPIs

The Windows platform accessibility API UI Automation has an upper limit for heading levels, specifically nine (9). I did no digging into Microsoft Active Accessibility (MSAA) nor IAccessible2 (IA2), two other (older) AAPIs on Windows.

Now to get to why Chromium opted not to fix aria-level for headings. Because of the UIA limitation, Chromium explicitly limits heading levels to 9. As a result, an issue was filed to have Core-AAM limit aria-level on headings to 9. No change was made and no level restriction is listed at Core-AAM (see the ARIA note above).

Because heading levels from HTML and ARIA are exposed through the same AAPIs, this will impact the proposed headingstart attribute as well as aria-level.

Screen Readers

The place where this all mostly shakes out is how heading levels are exposed to screen readers and then by screen readers to users.

NVDA, VoiceOver (macOS and iPadOS), and Orca all seem to handle values up to 231−1, which are provided by the browser. Invalid values are handled by the browser and passed through as level 2 (VoiceOver will give no level) or the <h#> element level.

JAWS cannot handle any level above 6, as filed in 2019. Perhaps a bit more troubling is that JAWS may ignore aria-level on <h#> elements.

TalkBack does not like levels above 6. When paired with Firefox it announces no level, but lean’s on Chrome’s bug to announce 2.

In Narrator, any headings (via HTML or ARIA) with a level greater than 9 are no longer announced as headings. If you have an invalid aria-level, it may concatenate the following content with it and announce it all as a heading at level 2.

VoiceOver headings list showing what level is announced for each heading in Safari. Narrator heading list showing two examples of concatenated headings.
VoiceOver (paired with Safari) shows the heading level alongside each heading. Narrator also shows the numbers and also demonstrates how headings O, P, and Q are merged into one, as well as R, S, and T.
NVDA headings list, first of two images, that shows visual nesting levels for headings but not their number. Second image of NVDA headings list, using Firefox.
NVDA uses a nested tree to convey heading levels and their implied relationships.
JAWS heading list showing the first third of headings on the page, mostly one through six. JAWS heading list showing the middle third of headings, mostly twos and sixes. The final third of headings on the page, also mostly twos and sixes.
The headings list in JAWS, split across three screen shots to fit them all. This is an at-a-glance confirmation that JAWS will not go past level 6 and often panic-drops to level 2.

Tests

You don’t need to take my word for it. I made a test page (debug mode) and included my results. My testing kit for this post:

Firefox 125
NVDA 2024.1
Orca on Ubuntu 22.04.2 LTS
Narrator Win11 23H2
TalkBack 14.2
Chrome 124
JAWS 2024.2403.3
NVDA 2024.1
TalkBack 14.2
Edge 124
Narrator Win11 23H2
Safari 17.4
VoiceOver macOS 14.4.1
VoiceOver iPadOS 17.4.1

See the Pen ARIA Heading Level Test by Adrian Roselli (@aardrian) on CodePen.

Verdict

The JAWS heading list dialog with a yellow three-in-one surface level resting on top.
Get the pun? A surface level on a dialog showing (mostly flat) levels.
  1. Until JAWS and TalkBack fix their bugs, probably don’t do / allow any heading levels higher than 6.
  2. And then, unless UIA (and Chrome and probably Narrator) changes its limitation, probably don’t do / allow any heading levels higher than 9.
  3. Using a heading (probably <h6>) with aria-level for any levels above 6 is safer than roleing up another element with aria-level.
  4. If you are hoping headingstart will resolve your heading level problems in a hands-off way with imported or user-generated content, you may end up creating confusion and WCAG risk for the reasons listed in this post (and maybe others).
  5. If you need more than two billion one-hundred forty-seven million four-hundred eighty-three thousand six-hundred forty-seven heading levels, you may need to hire a copywriter.

2 Comments

Reply

Typo? “for it so stand a chance”.

Dan Tripp; . Permalink
In response to Dan Tripp. Reply

It was, thanks!

Leave a Comment or Response

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>