Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Consider adding a headinglevelstart attribute #5033

Open
annevk opened this issue Oct 22, 2019 · 96 comments
Open

Consider adding a headinglevelstart attribute #5033

annevk opened this issue Oct 22, 2019 · 96 comments
Labels
accessibility Affects accessibility addition/proposal New features or enhancements

Comments

@annevk
Copy link
Member

annevk commented Oct 22, 2019

See the suggestion by @muan at #3499 (comment):

<h1>GitHub</h1>
<h2>jsdom/jsdom</h2>
<div>
<article headinglevelstart="3">
   <h1>jsdom</h1>
   <h2>Basic usage</h2>
   <h2>Customizing jsdom</h2>
   <h3>Simple options</h3>
   ...
 </article>
</div>

cc @whatwg/a11y

@annevk annevk added addition/proposal New features or enhancements accessibility Affects accessibility labels Oct 22, 2019
@scottaohara
Copy link
Collaborator

scottaohara commented Oct 22, 2019

Reading through the other thread, I'd prefer this approach of an opt in instead of just overwriting heading levels.

The only question I have is what is the expectation for something like:

<h1>GitHub</h1>
<h2>jsdom/jsdom</h2>
<div>
<article headinglevelstart="5">
   <h1>jsdom</h1>  
   <h2>Basic usage</h2>
   <h2>Customizing jsdom</h2>
   <h3>Simple options</h3>
   ...
 </article>
</div>

Expose h5, h6, h7 instead? h7 obviously being the outlier here.

Quickly looking at some of the different exposed levels from the platform mappings / trees exposed by browsers, a level 7 should work, but JAWS and NVDA range from getting a little tripped up at times, to JAWS with Firefox just not exposing the levels beyond 6 at all, instead reverting to level 2 or the default level of the heading element used.

Again, it seems the screen readers would need to be updated to account for heading levels beyond 6. Unfortunately, that also means that anyone using an older screen reader, even with a newer browser, may well not get optimal output.

@jimmyfrasche
Copy link

This would solve all my problems with hN tags: user/plugin generated content in templates and template partial that use hN tags. The template including any of the above can just tell it where to start.

@aardrian
Copy link

aardrian commented Oct 28, 2019

If an author can identify what value needs to be set in headinglevelstart, then it stands to reason the author can increment that in their own code for each descendant heading instead of offloading that burden to the user agent.

Pseudo-code with structure borrowed from above:

<h1>GitHub</h1>
<h2>jsdom/jsdom</h2>
<div>
<article>
   <h{1   $headinglevelstart}>jsdom</h{1   $headinglevelstart}>  
   <h{2   $headinglevelstart}>Basic usage</h{2   $headinglevelstart}>
   <h{3   $headinglevelstart}>Customizing jsdom</h{3   $headinglevelstart}>
   <h{3   $headinglevelstart}>Simple options</h{3   $headinglevelstart}>
   ...
 </article>
</div>

In other words, the headinglevelstart attribute on its own does not seem to offer anything a developer cannot do now.

For example, in this Codepen example from 2016 I use data-level to do seemingly exactly what this attribute proposes (though I would prefer a server-side solution over client-side or in the UA).

Though neither approach prevents an author from choosing a value that renders as <h7> or above, completely nullifying the benefit for many screen reader users (see @scottaohara's comment above).

@LJWatson
Copy link

LJWatson commented Oct 28, 2019

@aardrian wrote:

If an author can identify what value needs to be set in headinglevelstart, then it stands to reason the author can increment that in their own code for each descendant heading instead of offloading that burden to the user agent.>

The priority of constituencies puts users and authors before implementors, and this seems to be one of the times when that prioritisation is needed. Authors struggle with heading levels as it is, and this has an impact on users. If we ask authors to take on handling more, I'm afraid things will only get harder for authors and worse for users.

If the UA does the work, it has other advantages too:

  • Parameters can be set to handle things like heading levels beyond six.
  • Simpler authoring code is less error prone and less likely to break.
  • Less authoring code may help performance.
  • Enables progressive enhancement.
  • Familiar technique (as @Dan503 noted).

On this last note, would it be possible to extend the capability of the start attribute, instead of minting a new headinglevelstart attribute?

@Dan503
Copy link

Dan503 commented Oct 28, 2019

Would it be possible to extend the capability of the start attribute

I like how short the "start" attribute is but if it only contains a number in it, it isn't as clear what it is doing as it is on ordered lists.

If you have <article start="h3"> though, that is pretty clear what the intention is.

@patrickhlauke
Copy link
Member

If an author can identify what value needs to be set in headinglevelstart, then it stands to reason the author can increment that in their own code for each descendant heading instead of offloading that burden to the user agent.

In fairness, from a development point of view, using headinglevelstart would not require potential reprocessing of content. Assuming in the example above the actual <article> content comes from somewhere else (stored like that in a database, coming from a third-party via an API call), the author can simply define the template

<h1>GitHub</h1>
<h2>jsdom/jsdom</h2>
<div>
  <article headinglevelstart="3">
    {INSERT THE STUFF HERE}
  </article>
</div>

without having to server-side re-munge the content to reprocess all heading levels appropriately. Otherwise, particularly for third-party stuff, they'd have to always proxy content and do, in naive terms, do a find/replace of any heading elements (either native <hx> ones or ARIA-based headings and their aria-level="x") to bump their number.

@aardrian
Copy link

@LJWatson, to your point about the Priority of Constituencies, I feel that pushing the logic to the UA creates a black box that will make it easier for developers to ignore, resulting in a generally worse experience for users. And I also want a better experience for users (over authors).

@patrickhlauke, that is a good point (and also made by @LJWatson). And yes, the client-side processing is potentially more burden for users than broken / opaque headings.

Perhaps something similar to the code/approach I posted above could be the basis for a polyfill (though it still won't help current SRs).

@bkardell
Copy link
Contributor

bkardell commented Oct 28, 2019

This mostly addresses what I was interested in in this comment a while ago - though, it still seems to me we could probably just figure that out with simply a marker attribute rather than an explicit level, and that would be better for authors. Still, this very much seems like progress and worth doing if that's not plausible.

@jimmyfrasche
Copy link

@aardrian

If an author can identify what value needs to be set in headinglevelstart, then it stands to reason the author can increment that in their own code for each descendant heading instead of offloading that burden to the user agent.

That is simple for simple cases but it gets more complicated quickly.

The approach you outlined works fine within a single template file whose content is all under the control of the author (but, in that limited context, so does just using h-tags, which are also easier to read).

Here is where it hurts:

When a template includes another template the counter needs to be both threaded through the template and used by the child templates as well. Many templating languages support this, though not all make it super pleasant, but it's not of much use unless the entire project agrees upon the convention. That's easy on greenfield projects but legacy code requires more work or when there's a "parent theme" and you end up needing to modify all of its templates even if you're making no other changes. On hosted platforms the choice of templating language is usually fixed so whether this can be done at all can be out of one's hands.

Sometimes html comes from a blackbox plugin. This is no one's favorite but it happens distressingly often. The generated html can be parsed and its headers shifted by walking the AST allowing it to be re-rendered correctly, but this is fairly complicated and slow so it's unlikely to happen and probably needs to be fixed up on the client side with some javascript.

If the platform had the concept of the heading level baked into its core and took care of threading it through templates and plugins, all of which agreed to use this feature, this wouldn't be as much of an effort. To my knowledge, there is no such platform or at least it is not among the most commonly used. Even if all the popular ones added it today it would take quite a while for it to see widespread use and for legacy code bases to get on board even if their platform now supported it.

That still leaves user-generated content. If it's stored in some non-html source or some IR that can make it easy to generate starting at whatever level is necessary (assuming it's an option: it usually is not). Often it's stored in html that was created by some kind of WYSIWYG editor. If every instance of the user generated content starts at, say, h2, you could configure the header to not allow creating h1 tags. If this is a legacy project or a project going under redesign that means going through the database and parsing all the entries and shifting the headings. Not pleasant but it's a one time thing. If you're trying to use the content where the starting header is different in different contexts you're back to fixing it up on the fly.

(I haven't looked into it too deeply yet, but it seems like wordpress's gutenberg would have many of these problems simultaneously).

None of this is insurmountable but it's enough to surmount that it doesn't seem to ever get surmounted.

Having an attribute may mean occasionally having to add a div to hang it off of but the outlining solutions could also mean having to insert an extraneous tag and generally one of greater semantic weight and its less explicit that those tags are necessary to maintain the correct document outline. An attribute would also make it easier to fix up a legacy codebase with just a few simple edits.

Another thing I like about having an attribute is that it could possibly have extra modes in the future like "none" to mean "ignore any headings in this subtree" (useful for teasers whose teased body should not be contributing to the document outline) or something to mean "this came from an editor apply a slower algorithm to shift the headings so that are no gaps like h3 to h6 with no intervening h5".

@aardrian
Copy link

@jimmyfrasche, I feel bad that my response is so brief as to seem dismissive, but I understand your points and agree with many. After my last comment I am not pushing my model anymore as I made my case (and nobody seems down with it). But I still think a polyfill may be helpful.

@muan
Copy link
Member

muan commented Sep 15, 2023

Notes from TPAC, slide for context.

What this proposal do: help web authors prevent user generated content from breaking the page heading structure.
What this proposal doesn't do: prevent misuse of heading levels and skipping levels.

To clarify, this is the expected affect:

Code

<h1>jsdom/jsdom</h1>
<h2>Files</h2>
<h2>README.md</h2>
<div headinglevelstart="3"><!-- user generated content starts -->
  <h1>jsdom</h1>
  <h2>Basic usage</h2>
  <h2>Customizing jsdom</h2>
  ...
</div>
<h2>About</h2>
  <h3>Resources</h3>
  <h3>Licenses</h3>
<h2>Releases</h2>
...

Heading structure

h1 jsdom/jsdom
	h2 Files
	h2 README.md
		h3 jsdom
			h4 Basic usage
			h4 Customizing jsdom
	h2 About
		h3 Resources
		h3 licenses
	h2 Releases

with an addition to the proposal:

interface HTMLHeadingElement {
  [CEReactions] readonly attribute unsigned long level;
};

In case of

<div headinglevelstart="2">
  <article headinglevelstart="6">
    <h1><h1>
    <h2 aria-level="1"><h2>
  </article>
</div>
$ h1.level // AAM level 7
7
$ h2.level // AAM level 1
8
$ h2.ariaLevel
1

Todo:

  • Naming bikeshed?
    • At TPAC it was suggested that headinglevelstart is good in that it implies this only looks at the closest parent element
  • Polyfill or explore the possibility for an experimental feature to get AT user feedback
  • Getting standards positions ( 1 from Chrome from TPAC)
  • Draft spec

Future to do:

@smaug----
Copy link

Also discussed at TPAC: need to define how this works with Shadow DOM. Most like the level should be computed based on the shadow including ancestor chain.

@scottaohara
Copy link
Collaborator

scottaohara commented Sep 19, 2023

For a lot of use cases, this will probably be fine as long as the headings remain in the 1 to 6 level range. But things are slightly better than I mentioned 4 years ago with browser/screen reader support for levels beyond 6.

Safari and Firefox expose the specified level if one goes beyond level 6, but Chrome/Edge appear to cap out at level 9, and beyond that heading levels get exposed as a level 2 regardless of the specified value.

Vispero/JAWS would need to be looped in on this, as levels beyond 6 get treated as level 2 - regardless of if the browser exposes the specified level or not. I also noticed that NVDA doesn't include headings between 7 and 9 when navigating with the H key with Chrome, but seems to do it fine with Firefox. So, also something they should be looped in on, as well.

I mention all of the above specifically in context to:

Polyfill or explore the possibility for an experimental feature to get AT user feedback

Since a polyfill won't be enough without Chrome and Edge/UIA being modified to expose levels beyond 9, and JAWS changing their treatment of higher levels.

Fairly recently, the Editor's draft for ARIA included this note for aria-level

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.

The important part of that note - as support for higher levels can obviously change with work - is more the fact that the higher the potential heading level, the more difficult the content may be to understand/navigate for someone using a screen reader.

So with that said, I'm supportive of the intent behind this attribute and how it can make heading levels easier to adjust for developers when necessary, but was there any talk of a level cap for this attribute at TPAC? Making sure all browsers/screen readers support up to level 9 seems reasonable. That's at least what the ARIA wg discussed when creating that note. But beyond level 9 maybe seems a bit much? Someone being able to declare headinglevelstart="43" seems a bad idea.

cc @aleventhal @jnurthen

@patrickhlauke
Copy link
Member

But beyond level 9 maybe seems a bit much?

I suspect that as soon as you introduce a reasonable-sounding cap, somebody will come along with a particularly complex document/site that requires one more than the cap...

@scottaohara
Copy link
Collaborator

scottaohara commented Sep 19, 2023

I mean, they must be getting by somehow right now, with their six-level limitation, though maybe they're not. so yeah, that's fair point @patrickhlauke. Author guidance to allude to the potential cons then, at least?

@muan
Copy link
Member

muan commented Sep 19, 2023

but was there any talk of a level cap for this attribute at TPAC? Making sure all browsers/screen readers support up to level 9 seems reasonable. That's at least what the ARIA wg discussed when creating that note. But beyond level 9 maybe seems a bit much? Someone being able to declare headinglevelstart="43" seems a bad idea.

Yes, @mcking65 and @spectranaut was at the WHATWG meeting. Matt brought up exactly this issue. I am personally not opposed to capping the levels but agree as @patrickhlauke said.

The level problem was why future to do (as author guidance) was included, since user misuse of the attributes (aria-level now, headinglevelstart in the future) were mostly going to be inevitable:

Future to do:

Happy to add a cap in a draft spec, but regardless I hope it won't block this proposal, as for the alternative seems to be what led to this proposal to begin with 🙈 .


Since a polyfill won't be enough without Chrome and Edge/UIA being modified to expose levels beyond 9, and JAWS changing their treatment of higher levels.

FWIW my plan was to leave this up to further feedback when I create the polyfill, with capping at 6 or 9, or none.

@scottaohara
Copy link
Collaborator

thanks @muan - yeh i saw the 'future to do', but i would just submit that author guidance should be added with the proposed spec update and also keep the future to do for checkers to consider adding guidance as well. Whatever guidance is added would be contingent on the decision to cap or not, but that shouldn't be a blocker.

@muan
Copy link
Member

muan commented Sep 21, 2023

I've initialized a polyfill with some open issues. Feel free to open issues and let me know if you'd like to be a contributor! I'll carry on with the rest of the to-dos.

@tomByrer
Copy link

Does not seem that Firefox & Chromium have any inherent CSS formatting for <h7> markup; currently inline / default / ignored. So that will have to be added (though current <h6> somehow became too small by default).
https://codepen.io/tomByrer/pen/abPYdoG

Paging @jakearchibald; IIRC years ago he wanted something like <h> tags with auto-levels? Or maybe argued the opposite?

@muan
Copy link
Member

muan commented Sep 25, 2023

@tomByrer FWIW with this proposal there is never going to be <h7>, the only difference is that regular h* tags will be exposed to AT as level up to 9.

for styling @annevk had previously suggested a functional pseudo-class selector.

@annevk
Copy link
Member Author

annevk commented Sep 25, 2023

I think we should add :heading as well, as per my prior proposal.

@jakearchibald
Copy link
Contributor

jakearchibald commented Sep 25, 2023

Would <div headinglevelstart> work? As in, can there be an 'auto' value?

<div headinglevelstart="2">
  <h1>Hello</h1>
  
  <div headinglevelstart>
    <h1>World</h1>
  </div>
</div>

Where <h1>World</h1> would be level 3, since the auto value of headinglevelstart would be 1 of the parent headinglevelstart.

@Dan503
Copy link

Dan503 commented Sep 25, 2023

@muan

h* tags will be exposed to AT as level up to 9.

Why only up to level 9?
Why does there have to be any limit at all?

I see having a limit of any kind as an unnecessary restriction on what authors can implement in their layouts.

@jimmyfrasche
Copy link

I think we're saying the same thing but in slightly different ways.

Let's start with one headinglevelstart. I'll use the same example with some minor changes. For now let's focus on:

<div headinglevelstart=4>
  <h1>aria-level=?</h1>
</div>

headinglevelstart=n sets nested h1 to n, h2 to n 1 and so on. In this case since it's headinglevelstart=4 the h1 has aria-level=4.

We do want to think of this in terms of sums but 4 1 = 5 so we need to subtract 1. It will ultimately make the most sense to subtract it from the headinglevelstart value so we have (4-1) 1 = 4. Obviously this works for other h-tags so for an h2 we have (4-1) 2 = 5.

Let's add the second layer back in:

<div headinglevelstart=2>
  <h1>aria-level=?</h1>
  <h2>aria-level=?</h2>
  <h3>aria-level=?</h3>
  <div headinglevelstart=4>
   <h1>aria-level=?</h1>
 </div>
</div>

for the first three tags we have

h1: (2-1)   1 = 2
h2: (2-1)   2 = 3
h3: (2-1)   3 = 4

If we don't consider nested headinglevelstart then the next h1 is (4-1) 1 = 4 but that puts it at the same level as the prior h3 tag when it was meant to be subordinate.

However if we sum all the ancestral headinglevelstarts we have (2-1) (4-1) 1 = 5. The computed value of the aria-level is perhaps not immediately intuitive but the important thing is that it's what was intended and that the result is compositional.

If you remove the outermost div[headinglevelstart] the computed aria levels differ but they all maintain the same relationship. If you add a third [headinglevelstart] they all maintain the same relationship.

@tabatkins
Copy link
Contributor

Specifically, this is the example of nested contexts I gave, where you pretty clearly (imo) need the level changes to be "local".

So in the example code being worked with here, I feel fairly strongly you should get:

<div headinglevelstart=2>
  <!-- aka, interpret contained <h1>s as if they were an h2 in the outer context,
          and lower headings adjust accordingly -->
  <h1>aria-level=2</h1>
  <h2>aria-level=3</h2>
  <h3>aria-level=4</h3>
  <div headinglevelstart=4>
   <!-- aka, interpret contained <h1>s as if they were an h4 in the outer context (now h5 in the global context) -->
   <h1>aria-level=5</h1>
 </div>
</div>

Instead writing these as headinglevelstart= 1 and headinglevelstart= 3 would be acceptable, and make the accumulative behavior really obvious, I think. The only downside is that you're having to do the mental step of handling the 1-indexed nature of headings, so if you want your component to act like it's got h4s you need 3, etc. I don't have a strong opinion on which way to spell it.

(In either case, tho, making the value relative to the nearest ancestor headinglevelstart is definitely the way to go imo.)


The only issue with the above (with either spelling) is that you must set the value on the outer template, as you need to know precisely where in the outer template's heading structure the inner template is being placed. I think this is acceptable, fwiw, but if we did want to allow the inner component to opt itself into heading adjustment instead, we would indeed need an auto value that just adds 1 to the current heading level, per the outline algorithm.

@jimmyfrasche
Copy link

Maybe headinglevelshift=n would be better than headinglevelstart= n. The name makes the -ness of the operation and its subsequent accumulative effect clear.

@keithamus
Copy link
Contributor

keithamus commented Apr 11, 2024

So @smockle and I at GitHub are prototyping this out (https://groups.google.com/a/chromium.org/g/blink-dev/c/8yl-pJhuLHE/m/1GDufCYWAAAJ). We have some various points to raise and discuss. The devil, as the say, is in the details.

Any opinions weakly held, we'd like to hear feedback from y'all.

Naming

We went for headingstart as it seems less wordy. I'm also open to calling it headingshift but would like more opinions here.

After further discussion below, headingoffset is the current front-runner for the attribute name.

Microsyntaxes

Right now we have implemented the IDL to reflect long values, likely "limited it to only non-negative numbers" as it seems something like headinglevelstart=-4 is not a desirable path.

This means you can put in 1 per @jakearchibald's suggestions. headinglevelstart= 1 will work the same as headinglevelstart=1. I know Jake's suggestion was more around mandating the syntax use to obviate the semantics of it being additive but it feels more important to us that the IDL reflect a long per existing semantics of IDL reflection, rather than adding a new microsyntax that developers then have to internalise. Again, happy to hear opinions on this.

Additive Model

There's been a lot of discussion about an "additive model" (where multiple containers accumulate) vs "absolute" (find the nearest and use that value). From an implementation standpoint either model is trivial (we tree walk, and we either stop at, or accumulate, on each node with the attribute). The real decision here is picking one and sticking with it, as it's not a decision that will be easy to change once implemented.

We went with the additive model during the prototype as it feels like that has more implications that need testing; the perf impact, the complexity on web developers side of things. This means the following:

<div headingoffset=1> <!-- h1s are now h2s and so on -->
  <h1><!-- Level 2 --></h1>
  <h2><!-- Level 3 --></h2>
  <div headingoffset=2> <!-- h1s are now h4s -->
    <h1><!-- Level 4 --></h1>
    <h2><!-- Level 5 --></h2>
  </div>
</div>

An auto value

Auto values have been discussed. There are some large concerns on our side with this. One path is that auto is a sentinel value for 1, so that each container increments the heading structure accordingly:

<div headingoffset=auto>
  <h1><!-- Level 1 --></h1>
  <div headingoffset=auto>
    <h1><!-- Level 2 --></h1>
  </div>
</div>

The issue is that for simple demos this looks fine but it gets a bit funky if you have real outlines:

<div headingoffset=auto>
  <h1><!-- Level 1 --></h1>
  <h2><!-- Level 2 --></h2>
  <h3><!-- Level 3 --></h3>
  <div headingoffset=auto>
    <h1><!-- Level 2 --></h1>
  </div>
</div>

This feels broken from an AT user perspective because they're not traversing down a heading structure, in fact it feels like not much of an improvement at all. Consequently, it feels like we'd want auto to be more "magical", by determining the shift based on the heading structure of the parent container:

<div headingoffset=auto>
  <h1><!-- Level 1 --></h1>
  <h2><!-- Level 2 --></h2>
  <h3><!-- Level 3 --></h3>
  <div headingoffset=auto>
    <h1><!-- Level 4! --></h1>
  </div>
</div>

This worries us from both a complexity and performance perspective. Effectively auto would need to span its descendent tree and find the largest heading element. Perhaps more experienced spec authors/implementers can tell us how actually troublesome this would be but our hunch is that it's not great.

What to do about aria attributes?

We feel fairly strongly that aria-level shouldn't be meddled with for various reasons that I'm happy to outline but for brevity I'll skip here. Consequently aria-level will always be absolute. If anyone feels strongly against this, please speak now.

<div headingoffset=4>
  <h1><!-- Level 5 --></h1>
  <h1 aria-level=1><!-- Level 1 --></h2>
</div>

What's slightly less clear is what to do with a role=heading with no aria-level. ARIA 1.1 says the fallback level should be 2. So:

<div>
  <div role=heading><!-- Level 2 --></div>
</div>

What happens if we introduce a headingoffset= to this? Should it remain 2? Or should it be included in the additive model? For the purposes of the prototype we decided to include it. This means:

<div headingoffset=1>
  <h1><!-- Level 2 --></h1>
  <div role=heading><!-- Level 3 --></div>
  <div headingoffset=2>
    <h1><!-- Level 4 --></h1>
    <div role=heading><!-- Level 5? --></div>
  </div>
</div>

@annevk
Copy link
Member Author

annevk commented Apr 11, 2024

Thanks for moving this forward! Some thoughts:

  1. You discuss reflecting upfront but then consider the addition of an auto value which has no precedent in reflecting. I recommend starting with the scenarios you want to address and what a reasonable API might be and working backwards from there as to how to best represent it in HTML.
  2. Note that if you want to allow 1 or even encourage it there's also conformance implications. As it stands today a valid non-negative integer does not include a .
  3. HTML semantics should not directly impact ARIA. If role=heading is always level 2 it should remain that way. If we start mixing that here people will start expecting it elsewhere and we'll get a rather confusing mess of things.

(Overall, I'd probably stick with additive, using ordinary HTML integers, and drop auto and ARIA integration.)

@jakearchibald
Copy link
Contributor

(Overall, I'd probably stick with additive, using ordinary HTML integers, and drop auto and ARIA integration.)

1. Adjusting the naming with something like shift/offset seems good.

@keithamus
Copy link
Contributor

keithamus commented Apr 11, 2024

  1. You discuss reflecting upfront but then consider the addition of an auto value which has no precedent in reflecting. I recommend starting with the scenarios you want to address and what a reasonable API might be and working backwards from there as to how to best represent it in HTML.

Yes that's understood. I think we went in this direction. We assessed if auto would be a good option, counted it out due to complexity, then realised without auto we could simply reflect a value as long which makes the implementation far simpler.

I think if there's strong desire for auto we can move from [Reflect] long to a non-reflecting value with similar semantics, and still be backcompat right?

  1. Note that if you want to allow 1 or even encourage it there's also conformance implications. As it stands today a valid non-negative integer does not include a .

Sounds good to me, if it sounds good to Jake too.

  1. HTML semantics should not directly impact ARIA. If role=heading is always level 2 it should remain that way. If we start mixing that here people will start expecting it elsewhere and we'll get a rather confusing mess of things.

That sounds like a good idea. We'll move forward under the assumption that this only impacts <h1>-<h6> that have otherwise not set an explicit role or aria-level.

(Overall, I'd probably stick with additive, using ordinary HTML integers, and drop auto and ARIA integration.)

👍

1. Adjusting the naming with something like shift/offset seems good.

👍 in which case we'll rename to headingshift in lieu of dissenting opinions forthwith.

@jakearchibald
Copy link
Contributor

Sounds good to me, if it sounds good to Jake too.

Yeah, I only used the to differentiate between absolute values and relative values. If it's always additive, and is named to suggest that, it's good.

@patrickhlauke
Copy link
Member

FWIW i have a slight preference for headingoffset rather than headingshift, but generally either works for me

@bkardell
Copy link
Contributor

It seems to me that if we have an additive model it is at least plausible to build new documents which follow a relatively simple strategy that affectively gives them an 'auto' value, but doesn't try to solve the (probably impossible?) task of making it possible to just slot that automatically into any existing document with messed up headings strewn all over the place. I am supportive of trying the additive model and I like both of the named versions being supplied above, and also have a (very) slight preference for headingoffset

@scottaohara
Copy link
Collaborator

thanks @keithamus (and all who have helped work on this!), this looks promising!

@annevk beat me to it, but absolutely agree that <div role=heading> not be included in this.

@joppekroon
Copy link

For me the frustration that I hope this proposal will solve is headings in components.

If you want to define a reusable part, sometimes that includes a heading or two. But it becomes a whole thing because as the author of the component you do not know where it will be used. So you have to build a way to deal with the headings to fit where it'll end up in the hierarchy, or give up on the whole idea.

It would be awesome if the browser could figure that out automatically. If that requires a property, that's fine. An auto value would be excellent, because you can just set it as the component author. If 'auto' is problematic for some reason, I guess it's okay if the user of the component has to set a specific offset value, but it has the disadvantage that it can be forgotten.

With regards to aria headings, why would they be treated any differently? A div with role heading and aria-level="3" is functionally an H3, so I'd expect it to be affected similarly. So in the odd situation where the aria-level is not provided and it defaults to an H2 like thing, why not treat it like an H2? You're explicitly opting in to the heading recalibration with the attribute, so it should not be surprising that it becomes a different level heading.

The one other thing I'm worried about is going beyond H6. With this attribute, it'll become very easy to recalibrate a heading to a level beyond 6. I believe the JAWS screen reader handles up to 10, anything beyond that will effectively become an H2, completely screwing with the heading structure. If I remember correctly NVDA ignores everything beyond 6. I think this proposal should include what should happen if the headings go beyond 6 (and 10).

While in websites you don't often go that deep in the heading structure, In more complex web applications it is not uncommon to have to have hard discussions about how to stick within the 6 available levels. Once you can add headings into components because you can offset them, it becomes a even harder to keep this under control.

@keithamus
Copy link
Contributor

thanks @keithamus (and all who have helped work on this!), this looks promising!

I am a humble facilitator. Credit goes to @muan for the proposal, everyone here for the wonderful discussion and @smockle for prototyping!

If you want to define a reusable part, sometimes that includes a heading or two. But it becomes a whole thing because as the author of the component you do not know where it will be used. So you have to build a way to deal with the headings to fit where it'll end up in the hierarchy, or give up on the whole idea.

In theory that's possible with this proposal I believe. If each component started its own heading outline with level 1, and set the containers it rendered its components into with a headingoffset appropriately scoped, then everything should flow. The main document would then also need to set the appropriate attributes as it incorporates headings.

<body headingoffset=0><!-- redundant, for illustrative purposes -->
 <h1>My wonderful component based website</h1>
 <my-component headingoffset=1>
  <template shadowrootmode=open>
    <h1>This is an h2</h1>
    <sub-component headingoffset=1>
     <h1>This is an h3</h1>
    </sub-component>
  </template>
 </my-component>
</body>

With regards to aria headings, why would they be treated any differently? A div with role heading and aria-level="3" is functionally an H3, so I'd expect it to be affected similarly. So in the odd situation where the aria-level is not provided and it defaults to an H2 like thing, why not treat it like an H2? You're explicitly opting in to the heading re-calibration with the attribute, so it should not be surprising that it becomes a different level heading.

I think this logic is reversed from reality. All headings are functionally identical aside from their localname, which is used to derive the level they expose to AT. In other words an <h3> is more like an <div> with an implicit role=heading aria-level=3. As an example of that logic aria-level is a more explicit way to set the level than using the localname, to-wit a <h3 aria-level=4> is level 4, not 3. I also think there are a few more concrete reasons we'd want to avoid meddling with aria attributes:

  1. It goes against the spirit of ARIA, IMO. ARIA is meant as a set of attributes applied over the top of an html document in order to make the previously less-accessible nodes more-accessible. As such HTML as no influence over ARIA attributes (as an example for example UAs don't ship styling for [aria-level] while it does for h* elements). Once you set one, it is that thing (modulo the requirements/restrictions ARIA has).
  2. It reduces the number of avenues available to web developers to escape out of setting an implicit level. If (for some reason) you actually want to downshift levels, aria-level potentially offers that, if we leave it alone.
  3. (This is the least significant reason) it makes the feature effectively impossible to polyfill. Any polyfill would have to use aria-level and if we mess with it then we mess with any chance of polyfilling.

The one other thing I'm worried about is going beyond H6. With this attribute, it'll become very easy to recalibrate a heading to a level beyond 6. I believe the JAWS screen reader handles up to 10, anything beyond that will effectively become an H2, completely screwing with the heading structure. If I remember correctly NVDA ignores everything beyond 6. I think this proposal should include what should happen if the headings go beyond 6 (and 10).

Right now our prototype implementation clamps to a maximum level of 9. So <div headingoffset=9><h1></h1><h2></h2></div> is 2 level 9 headings, as is <div headingoffset=7><h3></h3><h4></h4></div>. This is likely what we'll spec around unless there's significant pushback on that.

@tabatkins
Copy link
Contributor

In your additive model example, you have the following:

<div headingstart= 2> <!-- h1s are now h2s and so on -->
  <h1><!-- Level 2 --></h1>
  <h2><!-- Level 3 --></h2>
  <div headingstart= 2> <!-- h1s are now h4s -->
    <h1><!-- Level 4 --></h1>
    <h2><!-- Level 5 --></h2>
  </div>
</div>

I don't understand this example - did you mistype some of the numbers? Your outer container has 2 but says "h1s are now h2s" (increase of 1), then your inner container also has 2 but says "h1s are now h4s" (increase of 3).

I presume you meant that, in the outer container, h1s become h3s, and in the inner, h1s become h5s, right? Or perhaps you meant for the outer container to have 1?

@keithamus
Copy link
Contributor

Yes sorry the example's first heading start should be 1. To rewrite the example with that change:

<div headingoffset=1> <!-- h1s are now h2s and so on -->
  <h1><!-- Level 2, h1   1 = 2 --></h1>
  <h2><!-- Level 3, h2   1 = 3 --></h2>
  <div headingoffset=2> <!-- h1s are now h4s -->
    <h1><!-- Level 4, h1   2   1 = 4 --></h1>
    <h2><!-- Level 5, h2   2   1 = 5 --></h2>
  </div>
</div>

@romainmenke
Copy link

romainmenke commented May 8, 2024

The problems we face building components and themes for a CMS are:

  1. when writing layout code we don't know the components or sequence of components that will be used
  2. when writing components we do not know the preceding or encapsulating components
  3. when writing components we do not know which text content or other components will be used in slots

To me it seems that this proposal only helps with problem 3.

<!-- we know this h2 is correct in this location -->
<h2>Some title</h2>
<div class="a-slot" headingoffset=2>
  <!-- we don't know anything about this -->
  {{ content }}
</div>

But it doesn't really help with the first two right?
We need to know the heading level we want to offset and not knowing this is the problem :)

Example:

<!-- this h3 was set by an editor and is unknown to us when writing code -->
<h3>Some title</h3>

<!-- this is component code we write -->
<!-- which offset do we use so that it is a part of the current chapter? -->
<div headingoffset=??>
  <h1>Other title</h1>
  <!-- component -->
</div>

I might be overlooking something in the latest proposal that does help with these issues?

@prlbr
Copy link

prlbr commented May 9, 2024

@romainmenke The proposal enables an author who uses your component to wrap the component in an element with an appropriate headingoffset attribute.

I don't think that getting the heading structure correct reliably is possible without an explicit statement from the author or a requirement for explicitly marked sections. Without it we can't know whether the component in an example such as

<h2>Section heading</h2>
<h3>Subsection heading</h3>
<p>Paragraph</p>
<the-component></the-component>

is intended to be inside the implicit subsection marked by <h3> or whether it is supposed to be outside that but inside the section marked by <h2> or whether it is intended to be outside of both.

@keithamus
Copy link
Contributor

I think you're right @romainmenke - if you plan on interleaving components within unknown content you'll need to somehow determine the heading level used in the unknown content. I'm not sure how this can be solved without ratcheting up the complexity, and I'm cautious that we definitely don't want to repeat the ill-fated document outline algorithm.

@romainmenke
Copy link

romainmenke commented May 9, 2024

The interleaving of unknown components is the direction that CMS's like WordPress have chosen. Given how much of the web is powered by these CMS's I do think it is important to try and solve it for these aswel.

But I appreciate that this implies significant complexity :)

@keithamus
Copy link
Contributor

I think it's important to de-scope that kind of work for the initial prototype due to the complexity. We can always come back and add something like headingoffset=auto if we can work out a viable path for resolving such a value.

@jakearchibald
Copy link
Contributor

jakearchibald commented May 9, 2024

Agreed. I think the best answer here is for WordPress to have a plugin to fill in the right value of headingoffset, but then it can ignore the content injected within.

(that doesn't mean there can't be a more complex feature later that removes the need for that plugin)

@romainmenke
Copy link

I think the best answer here is for WordPress to have a plugin to fill in the right value of headingoffset, but then it can ignore the content injected within.

Do you mean that a plugin (or any custom code in any CMS) provides some input field and that content editors set the right value?

If so, yes, I think that is the only viable way to use headingoffset in something like WordPress.

This could be paired with tools that show a more abstract document outline, like the Accessibility Tree view in Chrome dev tools. Content editors wouldn't need to know exactly how headingoffset works, only how to use the tool that shows them an outline and how to evaluate that. (Which is something that is valuable to be able to do even today.)

Giving content editors the tools to set the value and have good feedback on their document outline already helps end users a lot.

@keithamus
Copy link
Contributor

It's been a while since I worked on a CMS so I didn't want to speak outside my experience but my imagination is that if the thing we labelled as "unknown content" is actually content within a WYSIWYG editor within the CMS then it seems plausible for the CMS to, when a component it added, resolve the documents outline and inject an applicable headingoffset=N to the component. For example it could keep track of the last used heading level so that it can apply it to any components added after that.

@romainmenke
Copy link

Things like Gutenberg (the thing replacing TinyMCE in WordPress) allows you to nest components.

So you might have a rows component that itself can contain a list of any other component, including more rows.


Keeping track of the last used heading level is also something I am considering but I think it will require re-parsing all preceding content.

Any component can render whatever HTML it wants. There isn't a hard requirement that they must use a specific function to render headings or that they must trigger a callback. The only way to determine the previous heading with certainty is by buffering the output and re-parsing it.


Exposing headingoffset to content editors might be the better approach (for now). Only content editors can have a good view on the complete document they are creating.

@keithamus
Copy link
Contributor

One potential gotcha with a cumulative headingoffset we've discovered is that when applying it to a larger document as a replacement for <h2..6> tags, there are certain times when you need to reset - namely dialogs. Consider:

<main headingoffset=0>
  <h1>Settings</h1>
  <section headingoffset=1>
    <h1>Profile Settings</h1><!-- this is effectively h2 (1 1) -->
    ...
  </section>
  <section headingoffset=1>
    <h1>Account Settings</h1><!-- this is effectively h2 (1 1) -->
    <button invoketarget=delete_dialog>Delete my account</button>
    <dialog id=delete_dialog>
      <h1>Delete Account - Are you sure?</h1><!-- this is effectively h2 (1 1) -->
      <form method=dialog>
        <button type=submit>Yes</button>
      </form>   
    </dialog>
  </section>
</main>

In this example, ideally the content structure would be h1 "Settings", h2 "Profile Settings", h2 "Account Setings", and the <dialog> should contain an h1 "Delete Account - Are you sure?".

Potential solutions?

Unfortunately because headingoffset accumulates, and the h1 does a tree-walk, we're kind of stuck with an incorrect document structure that is potentially worse than the without the feature. Some viable options I see for a path forward:

Stop Tags

Allow elements to stop the tree walk - so if the check sees a <dialog> it stops tree walking and uses whatever result it has accumulated. Effectively:

let node = this;
let offset = 0;
while (node = node.parentElement) {
  if (node.localName == 'DIALOG') break;
  offset  = node.headingOffset;
}

Of course the downsides here are that the dialog tag name is somewhat arbitrary, it doesn't for example account for role=dialog. Also non-modal dialogs probably want to participate. I can see the if getting gradually more complex as more use cases appear.

Stop Attribute

Adding a headingstop or headingreset or headingoffset=reset or pleasestopaccumulatingtheheadingoffsetatthispoint attribute that acts as the same stop signal but in a more explicit & opt in manner, meaning the <dialog> needs to become <dialog headingstop>.

The downsides being that it's more markup to undo stuff that was already added via markup.

Allowing negative headingoffset

We could allow negative offsets to undo the shift from other elements. In the above example <dialog> needs to become <dialog headingoffset=-1>.

The downsides are more markup, and arguably more confusing markup, as well as this potentially not really solving the problem. The desire here is to reset the headingoffset to 0, but we're just translating that problem into a math problem instead. Yet another opportunity for off-by-one errors.

Go back to absolute headingstart

We can do the ops suggestion of using absolute levels, and avoid doing a cumulative tree walk. This way in the above example <dialog> would need to become <dialog headingoffset=0>.

The downsides are that we aren't doing cumulative offsets, which are quite useful.

Let developers figure it out: use aria-level

Developers can of course always set an absolute aria-level on every element. This way, in the above example, <dialog>'s <h1> would need an explicit aria-level... as would any other heading inside the dialog.

The downside is this probably creates the most markup of all solutions.

Let developers figure it out: move the dialog out of the container and into the top level of the DOM.

If the markup is adjusted so that the <dialog> is a direct descendant of <main> this problem effectively goes away. However for teams building micro frontends, or frankly many other component models, this is effectively a blocker as a lot of the time they won't have the facilities to put elements at arbitrary positions in the DOM.


I'm sure there are many other solutions to this. If anyone has any ideas, or preferences or points about the above, I'm all ears. I've written enough now though, and I'd rather other people write some words to counter balance the amount of words I've written.

@jimmyfrasche
Copy link

For a modal dialog I don't see what would be gained by doing anything other than always resetting to 0 and I can't think of other places where you would want to do that. Is it possible to just special case modal dialogs? Alt: add headingoffset=reset and make it implicit for dialogs unless otherwise specified.

@annevk
Copy link
Member Author

annevk commented May 13, 2024

As a reminder, if we do something special for the dialog element we shouldn't also do it for role=dialog. HTML semantics shouldn't be directly influenced by ARIA.

@jakearchibald
Copy link
Contributor

Seems reasonable to have a headingreset attribute. headingoffset=reset means that the reflecting property would need to return a string for a thing that's mostly numbers.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
accessibility Affects accessibility addition/proposal New features or enhancements
Development

No branches or pull requests