-
Notifications
You must be signed in to change notification settings - Fork 756
Description
The topic of allowing authors custom selectors has come up periodically, but we have no concrete proposal yet.
For custom elements, JS can define arbitrary states that can be matched through the :state() pseudo-class. I just proposed expanding :state() to regular elements as well (through a similar JS API).
I wonder if we could feed two birds with one scone and piggyback on :state() for custom selectors as well. We could have a @state rule that defines matching rules for a custom state:
@state heading {
matches: h1, h2, h3, h4, h5, h6, [role=heading];
}Now :state(heading) can be used to match any heading.
Detailed discussion
Multiple @state rules cascade
Instead of @state rules with the same id overwriting each other, they could be composed, providing another way to satisfy some of the #10222 use cases. Yes, it would require a double handshake, but perhaps authors can reach a convention for certain states, especially if there is only one mechanism for both CSS and WCs.
/* Root */
@state checkbox {
matches: input[type=checkbox];
}
@state checked {
matches: :checked;
}
/* Custom component */
@state checkbox {
matches: foo-checkbox;
}Note that <foo-checkbox> doesn't need to override anything about :state(checked). As long as it sets the proper state in its JS, it Just Works™.
States added via @state combine with ElementInternals.states (and the proposed Element.states)
The resulting states are a union. This means, if a component removes a state via JS but the same state is in effect through an @state rule, it still applies.
Use cases
- Maintainability around complex selectors (both in terms of brevity and having a single source of truth)
- Allowing the host page to provide extension points (e.g. by styling
:state(button)instead ofbutton, .button
Potential future improvements
- In L1, specificity would always be
0, 1, 0. In L2, aspecificitydescriptor could be used to override. - In L1, all selectors would be combined, and thus there is no way to exclude from the matching (except via a convention, e.g. including
:not(.exclude-this)in the selector). L2 could introduce descriptors for this (e.g. anexceptthat also cascades), or even a descriptor that makes the rule behave in a last-one-wins manner. - In L1, pseudo-elements are simply not allowed in
matches, just like they are not allowed in:is(). L2 could relax this. - In L1, these rules would be tree-scoped. L2 could introduce a descriptor for defining a custom state that also applies to all shadow trees.
Pros & Cons
- ✅ A single mechanism for both WC, elements, and CSS maximizes the odds of de facto conventions emerging, and reduces the API surface authors need to understand.
- ❌
:state()may be a fitting name for some selectors, but not so much for others - ❌ No way to have parameterized pseudo-classes (but that is an open problem for custom states as well)
Prior art
- Looks like @romainmenke also thought these should be the same: [selectors] same syntax for custom states and custom selectors #8094
- [selectors] Custom state pseudo class proposal #4805 seems relevant too