Skip to content

[selectors] Adding a :heading() selector for headingoffset? #10296

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

Open
keithamus opened this issue May 8, 2024 · 11 comments · May be fixed by #11836
Open

[selectors] Adding a :heading() selector for headingoffset? #10296

keithamus opened this issue May 8, 2024 · 11 comments · May be fixed by #11836
Labels
selectors-4 Current Work

Comments

@keithamus
Copy link
Member

whatwg/html#5033 proposed to add a new attribute (now called headingoffset) which can offset a containers exposing heading level by a given amount, for example <div headingoffset=1><h1></h1></div> renders a heading with a level 2 (1+1 = 2) in the accessibility tree.

This begs the question if it's possible to somehow correctly style elements according to their computed offset. Given headingoffset is accumulative (it does a flat tree walk) it can be quite difficult to appropriately select for such combinations.

So, here I am, proposing a pseudo selector to allow for selecting the computed heading level. I propose :heading(n) as a selector for heading elements exposes as level n. An <h1> (with no headingoffset parents) would then be selectable as either :heading(1) or h1, but for example <div headingoffset=1><div headingoffset=2><h1></h1></div></div> would be selectable as :heading(4) (1+2+1 = 4).

I want to stress that I am of the opinion that aria-level or role=heading should not impact :heading(); they are readily selectable today and I don't think aria should have that dramatic an impact on other selectors.

I think it would be really helpful for us to consider adjusting the UA stylesheets such that the existing for styles like h1{} are extended to h1, :heading(1){}. I know this looks scarily like the old document outline but as headingoffset is both explicit and opt in, I'm confident this doesn't introduce the same issus as document outlines.

Thoughts?

@Crissov
Copy link
Contributor

Crissov commented May 8, 2024

#351 is related: :level(). Incidentally, I recently started to think about combining hierarchical and nesting level again, but do not have a more concrete proposal yet.

#3596 would bring :role(heading) to the table, but has no concept of hierarchic level.

@LeaVerou
Copy link
Member

Huge +1 to this. Low-hanging fruit, and likely easy to implement (for UAs already implementing headingoffset).

@astearns astearns moved this to Regular agenda in CSSWG April 2025 meeting agenda Mar 27, 2025
@astearns astearns moved this from Regular agenda to By Topic in CSSWG April 2025 meeting agenda Mar 27, 2025
@astearns astearns moved this from By Topic to Wednesday Morning in CSSWG April 2025 meeting agenda Mar 27, 2025
@css-meeting-bot
Copy link
Member

The CSS Working Group just discussed [selectors] Adding a `:heading()` selector for headingoffset?, and agreed to the following:

  • RESOLVED: Add `:heading()` that accepts a comma-separated list of an+b expressions
  • RESOLVED: `:heading()` has the expected class-level specificity
The full IRC log of that discussion <emeyer> TabAtkins: There was a suggestion for adding a :heading to apply to all headings, and also to select to select things based onthe outline algorithm that eventually got dropped
<emeyer> …There are HTML attributes that let you adjust the level of headings in a subtree, so this is relevant again
<emeyer> …Anything we do here should be contigent on the HTML thing actually happens
<emeyer> …Proposal is to allow `:heading(an + b)` that applies to adjusted heading levels rather than tag names
<kizu> q+
<astearns> ack kizu
<emeyer> kizu: I support this
<TabAtkins> (comma-separated list of an+b expressions)
<emeyer> …Do we want to have a shortcut to allow `:heading(n)`
<emeyer> TabAtkins: Yes
<emeyer> astearns: Proposed resolution is allow comma-separated list of an+b expressions to apply to adjusted heading levels
<emeyer> …Seeing no objections,
<emeyer> RESOLVED: Add `:heading()` that accepts a comma-separated list of an+b expressions
<emeyer> TabAtkins: All the UA stylesheets apply to headings by tag names, which has tag-level specificty
<emeyer> …A `:heading()` would by default of class specificity
<emeyer> …Is that a problem?
<kbabbitt> q+
<astearns> ack kbabbitt
<emeyer> …Should we say `:heading()` has tag specificity rather than tag specificity?
<kizu> q+
<emeyer> kbabbitt: To the extent this would juggle rule application order, that would be confined to the UA stylesheet, right?
<emeyer> TabAtkins: Yes
<emeyer> kbabbitt: So the implementor could rejuggle styles
<emeyer> TabAtkins: Yes, but author styles might get overridden byu the new UA styles if this has class specificity
<emeyer> kbabbitt: Ah, so that’s the concern, is that clash
<emeyer> TabAtkins: Yes, it’s representative
<astearns> change my h1, h2, h3, h4, h5, h6{} rule to :heading{}
<emeyer> …of the general class of concerns
<kurt> *:heading()?
<emeyer> astearns: I think there’s good reason to define this as having tag specificity
<TabAtkins> (the workaround is `:is(div:not(*), :where(:heading))`)
<emeyer> kbabbitt: Do we have other pseudos that do this?
<emilio> q+
<emeyer> TabAtkins: No; we don’t have any pseudos that stand in for tag nakmes except maybe :any-link
<astearns> ack kizu
<emeyer> kizu: It’s basically an alias to the adjusted tag names, so tag specificity makes sense here
<astearns> ack emilio
<emeyer> q+
<kbabbitt> s/is that clash/is authors adopting the new pseudo and getting surprising results/
<emeyer> emilio: Have we checked if changing the UA style sheet will result in conflict?
<emeyer> …I agree that migrating to H-tags to this makes tag specificity appealing
<emeyer> …But having a pseudo with tag specificity is odd
<kizu> `:is(h1,h2,h3)` — also a pseudo-class with tag specificity, kinda
<emeyer> …What about attribute selectors?
<emeyer> TabAtkins: They’re exactly class level
<emeyer> emilio: …I’d rather keep to class specificity for consistency, unless we find out it breaks lots of things
<bramus> +1 on staying consistent
<emeyer> …I think that’s somewhat unlikely
<emeyer> astearns: I’m not that concerned about the user stylesheets
<emeyer> …There are likely to be UA rules that can’t use the new thing for whatever reason
<emeyer> …I’m worried about defining this thing and then making it so you can’t use it without knowing the special incantation
<emeyer> TabAtkins: I’m pretty evenly divided on this
<astearns> ack emeyer
<TabAtkins> scribe+
<kizu> one of workarounds: `:is(:not(h1, :not(h1)), :where(:heading(1, 2, 3)))`
<lea> What about role=heading aria-level=N?
<TabAtkins> emeyer: is there a way we can make it so that... you know how :where() doesn't have specificity implications, can we do something related so it has specificity of the things inside it?
<TabAtkins> emeyer: so :heading(1) that's equivlaent to `h1`, if I have :heading(n+4), can we have the thing in the parens convey the specificity.
<lea> That’s not like :where, :where has 0 soecificity. It’s more like :is or :not
<emeyer> TabAtkins: Ths issue is all things you can do here, you could do with `:is()`
<astearns> ack dbaron
<TabAtkins> (it's always tag-equivalent when you desugar it)
<emeyer> dbaron: I worry about the cost of making rules more complex for everybody
<emeyer> …For implementors, for authors
<emilio> 1+
<emeyer> …This feels like a case of adding more magic, and probably isn’t worth more magic
<emilio> +1*
<emeyer> …The additional overhead of learning and understanding isn’t worth it
<emeyer> …I don’t feel strongly about this, but that’s where I lean
<ntim> +1 to dbaron
<emeyer> astearns: The sense of the room is that making an exception is not motivated enough
<emeyer> …So we should open this to a wider audience to see how people will use this
<emeyer> TabAtkins: Like IDs exist only for IDs, tags exist to apply to tags, so it’s probably unlikely to cause issues in practice
<emeyer> …I think having :heading() being class specificity should be safe
<kizu> `.foo { … } h1 { … } <h1 class="foo">` — replacement here will not be safe
<emeyer> astearns: Since that’s what falls out of the definition, do we need a resolution?
<emeyer> TabAtkins: Let’s record the sense of the room
<emeyer> astearns: Objections?
<emeyer> (none)
<emeyer> RESOLVED: `:heading()` has the expected class-level specificity
<emeyer> astearns: Lea asked about ARIA heading roles
<emeyer> TabAtkins: That’s up to HTML, not our problem
<emeyer> astearns: Fair enough
<dbaron> I think in general aria attributes don't affect non-a11y things, though.
<TabAtkins> ("what are headings" and "what level are they" is a host-language problem)

@chriskirknielsen
Copy link

Seems this had been discussed outside of this specific issue in the past, but just for clarity: in this proposal, would an empty argument for :heading() open up the possibility of replacing a standard h1, h2, h3, h4, h5, h6 selector list?

The raised specificity is a nice feature IMO, and I'd advise authors use :is(h1, :where(:heading())) if they want to get back to an element-level specificity of 0,0,1 (or a similar workaround). For resets and such, the list of 6 elements seems fine: it's clearer, easier to read, and is shorter than other workarounds anyways.

@valtlai
Copy link
Contributor

valtlai commented Apr 3, 2025

Could we have a parentheses-less :heading with an element-level specificity? This way, the functional :heading() could have a different specificity if desired and there would be no need for any workarounds.

@keithamus
Copy link
Member Author

in this proposal, would an empty argument for :heading() open up the possibility of replacing a standard h1, h2, h3, h4, h5, h6 selector list?

Could we have a parentheses-less :heading with an element-level specificity?

My intent would be to also add the paren-less :heading with element-level specificity. That is what I have specified here: #11836 (which needs some changes given some of the proposed resolutions, but I'll update it next week some time).

@chriskirknielsen
Copy link

@keithamus Would :heading (with or without parentheses) not be a pseudo-class, and should it then by definition not also have a class-level specificity? While I fully understand why we'd want an exception here, that introduces inconsistency in the language.

That just seems a little weird, and I think that's mentioned in the IRC log as a gotcha to teach: "Pseudo-classes behave like classes and have the same specificity, except :heading because it represents a list of standard heading elements."

Let's say we had a special selector for form input elements like :field that mapped to input, textarea, select (and friends), that should then also behave as :heading to be consistent, right? The list of exceptions could then grow over time, potentially, and remembering exceptions in a language, whether programming like CSS or natural like English, can be fun, but is usually frustrating.

If it has to have an element-level specificity, then it would make more sense to be ::heading with double-colons as that is a pseudo-element and already has a 0,0,1 specificity (unless I'm making that up), and there is precedent for functional notation, such as ::view-transition-group(*). Though I recognise the weirdness of using a pseudo-element selector for actual elements…

Anyways, sorry, this is nit-picky and perhaps a little off-topic, just seems an important point to keep in mind. While I know CSS has its historical list of mistakes, moving forward, being consistent (the letters "CSS" are in the word) seems like a good idea.

PS: sorry for the abundant use of the word "specificity". 🫠

@astearns
Copy link
Member

astearns commented Apr 4, 2025

in this proposal, would an empty argument for :heading() open up the possibility of replacing a standard h1, h2, h3, h4, h5, h6 selector list?

Could we have a parentheses-less :heading with an element-level specificity?

My intent would be to also add the paren-less :heading with element-level specificity. That is what I have specified here: #11836 (which needs some changes given some of the proposed resolutions, but I'll update it next week some time).

My recollection is that we did want :heading as well as :heading().

But @keithamus I’m not seeing where your PR says anything about the specificity the new pseudo-element. The resolution above about making it class-level is meant for :heading with or without arguments, and I would object to having a switch that changed the specificity based on whether arguments were provided.

We did have folks arguing for tag-level specificity instead of class-level specificity overall, but “consistency with other pseudo-elements” won the case yesterday. We can always revisit the question, but please don’t add tag-level specificity to the spec until we have done that.

@keithamus
Copy link
Member Author

The resolution above about making it class-level is meant for :heading with or without arguments.

I miss-typed, I meant to say class-level not element-level.

I’m not seeing where your PR says anything about the specificity the new pseudo-element.

Right now it does not, I have yet to update it post-resolution. I will when I get back to my desk on Monday.

We can always revisit the question, but please don’t add tag-level specificity to the spec until we have done that.

Right, no need. It'll be class-level specificity for both.

@LeaVerou
Copy link
Member

LeaVerou commented Apr 6, 2025

I missed the issue in the f2f but I think tag-level specificity makes sense and may be worth the inconsistency, though that’s a weak opinion.
I do agree with @astearns that the presence of an argument changing specificity violates the principle of least surprise. If we were to do tag-level specificity on this, I’d have the argument add to it.

I don’t understand why we’d also add a pseudo-element. It seems that a pseudo-class suffices just fine.

@romainmenke
Copy link
Member

Is it possible that somewhere there was a bit of confusion between pseudo element vs. pseudo class and functional pseudo class and non-functional pseudo class?

I thought this proposal only adds a pseudo class, which can be written both in functional and non-functional notation?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
selectors-4 Current Work
Projects
Status: Thursday Morning
Development

Successfully merging a pull request may close this issue.

9 participants