[css-cascade] Evaluate cascade order of ::slotted and global styles in the same conditions
Description
According to the spec 3.2.4. Selecting Slot-Assigned Content: the ::slotted() pseudo-element:
The specificity of
::slotted()is that of a pseudo-element, plus the specificity of its argument.
In a previous conversation (https://github.com/w3c/csswg-drafts/issues/1915#issuecomment-535381522), it was clear and the solution proposed is to use !important but I think that the current solution is a bad Developer Experience when you apply it to real life.
Following the definition,::slotted(h1) should have a 0-0-2 specificity and h1 just 0-0-1. Then ::slotted(h1) should win without !important. But it seems they don't fight in the same arena and !important is required.
Example
Here is an that explains better the use case. Taking this HTML as a base. We want to use slots to take the advantage of HTML declaration, for example for SEO reasons.
<fancy-hero>
<"shado-dom">
<style>
:host {
background: #333;
}
:slotted(h1) {
color: #CCC;
}
</style>
</"shado-dom">
<h1>My site is awesome</h1>
</fancy-hero>
We would like to have some generic styles for headings and paragraphs but the web component would like to restyle them using the ::slotted() pseudo-element. As there are other h1 in other pages without fancy-hero wrapper, we have some generic styles like this:
h1 {
color: #333;
}
Then the color of my fancy-hero h1 changed to #333. The only way to preserve the fancy-hero h1 style from inside the fancy-hero is by applying !important
:host {
background: #333;
}
::slotted(h1) {
color: #CCC !important;
}
But once you do that, it's impossible to change from outside, to make my component customizable. And the only way is to use custom properties like this:
:host {
background: #333;
}
::slotted(h1) {
color: var(--hero-title-color, #CCC) !important;
}
Then I can customize my fancy-hero adding this styles into the global styles:
fancy-hero{
background: red;
--hero-title-color: white;
}
Extrapolating this use case to a complex component with multiple slots and much more properties, it's not viable. At this point we have two options:
- Use shadow DOM to encapsulate styles, but we lost the light DOM advantage.
- Set
!importantand declare custom properties in all styles properties of my component.
Proposal
I would like to propose that shadow DOM styles fight with the global styles in the same conditions. The cascade should be applied independently where the styles are defined.
Coming back to the specificity definition::slotted(h1) should have a 0-1-1 and h1 0-1-0. Then ::slotted(h1) should win without !important. This allows developers to create custom elements more reusable and easy to customize without dealing with thousand of custom properties and fill the code with !important in all properties.
Demo
https://webcomponents.dev/edit/eQFJPlOvQMIuUZj71ohV/README.md
I also found in this explanation that don't talk about specify.
https://www.w3.org/TR/css-cascade-5/#encapsulation-contexts
When comparing two declarations that are sourced from different encapsulation contexts, then for normal rules the declaration from the outer context wins, and for important rules the declaration from the inner context wins
To compare 2 declaration you don't take only the source of the encapsulation context. Specificity of the selector should be evaluated too.
I can definitely see the issue here but I'm not sure I agree with the solution, in part, because it results in potential breaking changes for existing code that has already worked around it. As I understood it, the slotted content was meant really to be a pointer to light DOM and not necessarily meant to change its styling.
That we can force a win by adding !important seems to be the workaround while still allowing !important from the light DOM to override even that. I'm not sure it's the best experience for those consuming the components rather than building them. π€
Perhaps there's a middle ground. An opt-into level specificity by the shadow DOM styles?
I know it could break the backward compatibility, and I don't suggest it as solution. I opened the issue to find a way to improve the DX and be able to use the light DOM comfortably.
The problem with important is that once you use important in your component styles they can't be override externally. Then you have to choose between weak components styles (without important) or fixed styles (with important). In the weak case, just with a * selector sternal styles can break your component visualization.
From my point of view, it's impossible to build a design system using light DOM because you have to put important in all your properties and if you want to open the possibility to override you have to declare one custom property per property. This make unusable the light DOM and it much easier solve it with Shadow DOM and part attribute, but you can't with form components.
We have to find a solution to make light DOM "stylable" easily.
One of the main reasons why I dislike !important so much here "as a solution" is that it forces the reliance on cascading order.
Imagine a web component and an extension of it:
class FooExt extends FooEl {
styles() {
return `
${super.styles}
::slotted(#foo) {
border-color: green !important;
}
`;
}
}
The border color will only be green if this CSS part is later in the cascade than super.styles, because they both have !important. However, it's much cleaner if I can use CSS specificity e.g. by using an ID selector (#foo). This makes things less fragile and gives developers more control in my opinion, because it's a lot easier in practice to fight specificity wars than to fight cascade wars.
@castastrophe
I'm not sure I agree with the solution, in part, because it results in potential breaking changes for existing code that has already worked around it
Currently, the only way of working around this is with !important so I don't think that the proposed solution would break that.
IMO current behavior is pretty broken.
::slotted(foo) is targeting foo children of this component, a global foo is just that, a global style, should be considered less specific. With the proposed solution, if you need to override it from light DOM, you can always do so by adding more specificity to your selector or with !important if you like that.
However, I wouldn't mind opt-in, if it's something simple like :host ::slotted(foo).
The problem with important is that once you use important in your component styles they can't be override externally.
You can override them externally with the use of !important again. Slotted styles are on par with other pseudo elements for specificity.
@castastrophe you can't. Take a look the Demo provided, as you can see "Slotted paragraph with !important in the internal and external styles ππ" is in green instead of pink trying to override styles with important in the external stylesheet.
https://studio.webcomponents.dev/edit/eQFJPlOvQMIuUZj71ohV/README.md?p=README.md
Nice example.
Funnily enough even the dev tools get confused:

This happens in both Chrome 103 and Firefox 103
@jorgecasar Let's simplify your example: https://codepen.io/castastrophe/pen/KKoXGQo
There seems to be custom styles applied to READMEs in the environment you shared and that interferes with the discussion of specificity and scope.
Typography styles are complex because they already penetrate the shadow DOM; they're some of the few properties that do.
I want to focus the example on the use of !important when it comes to styling the background color (which does not penetrate shadow DOM boundaries).
I've reduced the component's template to:
<style>
::slotted(*) {
background-color: yellow;
}
</style>
<slot></slot>
Now to override, we use:
<style>
my-content .important {
background-color: pink !important;
}
</style>
<my-content>
<p>Slotted paragraph</p>
<p class="important">Slotted paragraph with <code>!important</code></p>
</my-content>
I've used a simple selector here on my page so you can see that the specificity of the selector isn't as important either: just the component and the class we're targeting. You can see in my provided link that this works as I would expect.
I want to note, I would not expect a !important from within a slot to be able to be overwritten from the outside of the component.
I find a fairly major flaw in your original proposal though to be honest which is this statement I would like to propose that shadow DOM styles fight with the global styles in the same conditions. which I feel contradicts the exact goal of web components which is to separate Shadow DOM elements from the global cascade entirely.
@castastrophe
I want to note, I would not expect a !important from within a slot to be able to be overwritten from the outside of the component.
Then, we're pretty locked:
a. ::slotted() can specify styles which will only work if there no other applicable style set anywhere outside (no concept of specificity), something like a fallback in the absence of any style.
b. ::slotted() with !important will overwrite permanently, no way for outer styles to overwrite it back.
I find a fairly major flaw in your original proposal though to be honest which is this statement I would like to propose that shadow DOM styles fight with the global styles in the same conditions. which I feel contradicts the exact goal of web components which is to separate Shadow DOM elements from the global cascade entirely.
Then ::slotted() shouldn't exist and !important shouldn't be considered, right?
I think the point here is to have a useful ::slotted(), the current one is very limited because it forces you to never ever write global styles if you want slotted() styles to work.
Hmm slots and content projection are a very common use case and the exact contract between a slot and its slottable (that which is slotted in) is ambiguously defined imo, it's not clear to what extent the slottable should be influenced by the component. In my personal experience authoring and consuming components, the answer is "a fair amount". If you have some widget in which you can slot DOM, it does happen from time to time that the widget relies on needing to functionally style what's projected into it, in theory ::slotted is for this purpose but currently it just doesn't do that job well at all.
It's not about a global stylesheet vs encapsulated component distinction
I humbly suggest that there should be some sort of new mechanism to enable specific styles to slotted components without breaking the current behavior. Having to add "!important" on all ::slotted rules is a very ugly work-around. Web components are a breath of fresh air for improving old code-bases with cool new functionality, but it's also the old code-bases which tend to have a lot of weird global CSS rules.
When my boss asked me to override an input with border-width: 2px !important in a third-party web component library, I realized how tricky this kind of thing is in the case of ::slotted !important.
I had to spend a day cloning this repo and then fixing its build issues on Windows just to change the border of the input from 2px to 1px. π
Retitling because it was clear in https://github.com/w3c/csswg-drafts/issues/6867 that the problem with slotted is not the specificity but the cascade order rules. It seems the reason people don't hit this with :host is because those are not builtins.
We discussed a bit "what would the ideal cascade order of these be". It seems there was a desire of just letting specificity fight as usual, and only then sort by tree (that is, make the sort key, ignoring cascade layers, something like (specificity, tree, source order).
Another less breaking alternative would be something like an "important layer" kind of concept, where you'd sort by layer importance before sorting by tree. Cc @LeaVerou @keithamus @rniwa @tabatkins
We discussed this a fair bit during breakouts with @emilio @rniwa during TPAC, and we'd love to hear from @tabatkins and @fantasai.
Currently, ::slotted() is practically near-useless, as even CSS resets from the host page override component styles. I thought #7922 would fix this, but on its own itβs not enough: the core problem is this cascade order rule that defines that all shadow CSS has lower precedence than all light DOM CSS unless !important is used.
In my experience, you typically want more granularity than the all-or-nothing of the current situation: you donβt want component styles to override author styles specifically targeting those elements, but also you don't want generic catch-all author rules like ul to override component rules applying styles to e.g. lists within that specific component. Also, specificity can be more granularly tweaked either up or down, whereas there is no recourse when a cascade order rule that sits above specificity doesn't do what you want.
Intuitively, it seems that the kind of cascade order that would make sense here is to treat encapsulation context at the same precedence level as source order. @emilio's proposed "important layer" concept seems useful in its own right, but would have similar problems as !important if it were the only solution to this problem.
Fixing this may require an opt-in of some sort (possibly as a ShadowRootInit option), since at this point these selectors are used widely enough that fixing this could be a breaking change. Alternatively, if we see that ::slotted() is used sufficiently infrequently, we could simply define ::part() as having very high (or even infinite) specificity. I suspect that will be a no-go however.
Currently,
::slotted()is practically near-useless, as even CSS resets from the host page override component styles.
I want to focus on this particular point. In #10094, I've suggested introducing (something like) @layer !defaults as a way to indicate low-priority styles that should lose to any styles defined within a shadow context.
Even if that doesn't solve all use-cases, I think it would be helpful. I've been using this simple example to demonstrate the problem: * { margin:0 } in the document currently overrides the margin declaration in all :host/::slotted rules.
It seems a little strange to me that cascade layers (currently sorted after context) would have an internal mechanism for jumping ahead of context in the sort order. But I agree that something layer-like would be useful here.
And in this case, I think specifically named options (like the mentioned !important and/or !default layers) since custom names would introduce some larger coordination issues?
Any solution that expects the host page to just "behave" is not workable when you're building components that need to work in any page. :/
It can be both, right? A way to deprioritize "outer" styles, and a way to prioritize "inner" styles.
In https://github.com/w3c/csswg-drafts/issues/10094#issuecomment-2179369415, I've suggested a new @context at-rule for that.
Hypothetical example
<head>
<style>
/* this will cascade before all shadow-roots, requiring no opt-in from shadow-roots */
@context(first) {
*, ::before, ::after {
box-sizing: border-box; margin: 0; padding: 0;
}
}
</style>
</head>
<body>
<my-component>
<div>Slotted</div>
<template shadowrootmode="open">
<slot></slot>
<style>
/* this will cascade after all outer contexts, requiring no opt-in from host */
@context(last) {
:host { padding: 4px; }
:slotted(*) { margin: 4px; }
}
</style>
</template>
</my-component>
</body>
The exact syntax and names are debatable of course. The important thing I want to highlight is that this is a concern that lies above layers, specificity, source order, etc. It doesn't make sense to solve it at the level of specificity. It would be more appropriate to solve it using context or a new concept that sits above context-scoped layers.
The CSS Working Group just discussed [css-cascade] Evaluate cascade order of ::slotted and global styles in the same conditions, and agreed to the following:
-
ACTION: lea to create a meta issue on shadow DOM styling and what needs to be solved
The full IRC log of that discussion
<kbabbitt> [presentation logistics chatter]<kbabbitt> lea: looks like this is about well known problem in web components space of slotted having low specificity
<kbabbitt> ... anything from outside overwrites anything from shadow dom when they target same element
<kbabbitt> .
<kbabbitt> ... run in to same problem with host
<kbabbitt> ... less a problem with part since you need to be very specific
<kbabbitt> ... but these problems keep coming up with slotted and host
<kbabbitt> ... even a css reset on outside will override styles on host
<kbabbitt> ... this is proposing that shadow dom styles fight with global styles at same specificity
<kbabbitt> ... basically they expect that shadow dom styles have specificity to resolve conflights with light dom styles
<kbabbitt> ... which I think would make sense
<kbabbitt> ... but would not be web compatible
<kbabbitt> ... but problem needs solving
<kbabbitt> ... it means component cannot rely on styling itself
<kbabbitt> ... can't use !important since it's a separate origin
<kbabbitt> ... if you don't use !important, anything from shadow dom has more important
<kbabbitt> ... if you use !important everything has higher priority
<kbabbitt> ... none of these are ideal
<kbabbitt> ... you want component users to be able to style components
<kbabbitt> ... but also need to be able to provide defaults
<kbabbitt> ... this is something we keep running into
<kbabbitt> ... adthis point, I don't think it's web compatible to change
<kbabbitt> ... which brings us again to having slotted as a combinator
<kbabbitt> ... there's no way to override this, no escape hatch
<kbabbitt> ... no special layer people can use
<kbabbitt> astearns: do you have a solution to summarize?
<kbabbitt> lea: solution I proposed before was to make slotted a combinator
<kbabbitt> ... would also solve a bunch of other problems
<emilio> q+
<kbabbitt> ... another issue about this
<astearns> ack dbaron
<kbabbitt> ... that by itself doesn't solve, but it solves other problems and doesn't have web compat problem so it can have different specificity
<kbabbitt> dbaron: part of my intuition here is that one weird thing here is that if you want to intermix things that come from inside and outside component
<kbabbitt> ... and you need to somehow make up how those appear in order
<kbabbitt> ... because cascade at some level relies on order
<kbabbitt> ... and if we get rid of distinction between inside vs outside we need to define order
<lea> q?
<kbabbitt> ... one weird thing is that order isn't obvious from how stylesheets get linked to different pieces
<lea> q+
<kbabbitt> ... not obvious that this declaration came i the order before or after that one
<kbabbitt> ... this observation, off the cuff, makes me wonder if it would help to have a syntax or component to inject a set of rules into containing page at specific place in the order
<kbabbitt> ... almost certainly very beginning or very end of order
<kbabbitt> ... though presumably then those rules would only apply to component itself
<kbabbitt> ... thus you wouldn't need to worr about collisions
<kbabbitt> ... not entirely sure that makes sense, but not sure slotted combinator will solve
<lea> qq+
<kbabbitt> ... even with that, we need sensible proposal for order for cascade to work together
<astearns> ack lea
<Zakim> lea, you wanted to react to dbaron
<kbabbitt> lea: as I mentioned, slotted doesn't solve the problem, just gives us an avenue to work around web compat
<lea> s/slotted doesn't solve the problem/a slotted combinator doesn't solve the problem/
<kbabbitt> dbaron: I recognize that, just saying that even if we use that as a hook, we still need not just a defined order but a sensibly defined order
<lea> q+
<astearns> ack emilio
<kbabbitt> emilio: was going to comment on same line
<kbabbitt> ... don't think slotted combinator helps
<kbabbitt> ... first it can be mixed with other selectors, might care about host and whatnot
<kbabbitt> ... also becomes subtle to handle because you want to get all the rules that go in a particular spot together
<kbabbitt> ... poking at selector is not a great way to do that
<kbabbitt> ... potential alternative that could work and give some flexibility is some sort of wrapping at-rule
<kbabbitt> ... we need to define requirements of this properly
<kbabbitt> ... because you may want to inject at beginning or among other rules, or something else
<kbabbitt> ... I don't think we thought about specificity of slotted [??]
<kbabbitt> ... with intent of them competing for other spots
<kbabbitt> ... my point is, I think some way of grouping rules that you want to sort outside is probably an easier avenue to explore
<kbabbitt> ... easier to understand, this weird selector does same thing as this old selector but in subtly different way
<kbabbitt> ... also allows tweaking, inject beginning, inject at end, somewhere else
<kbabbitt> ... question is how this interacts with current cascade sorting parameters
<kbabbitt> ... might want to behave as source order, always after other scope, or before
<miriam> q+
<kbabbitt> .... assuming the thing people want is to make specificity work as if shadow dom wasn't involved
<astearns> ack lea
<kbabbitt> lea: speaking of requirements, one is that the surrounding page should not have to do anything weird for things to just work
<kbabbitt> ... some ideas floated at points were, special layer name, important layer, stuff like that
<kbabbitt> ... for component it's ok to do weird things
<kbabbitt> ... surrounding page should noy have to do anything for tyhings to just work
<kbabbitt> .. want to drop component into page and have it just work
<kbabbitt> ... also one thing to keep in mind defining layer is that it's not just one layer
<kbabbitt> ... nested shadow roots, components containing componentys
<kbabbitt> ... something can be in shadow dom but function as light dom for components it includes
<kbabbitt> ... whatevber we define needs to work in that context
<kbabbitt> ... don't think it matters what order we define, maybe everything after page css
<kbabbitt> ... or same kind of order as if you were using style or linke element where style appears
<kbabbitt> .
<kbabbitt> ... any order is better than no order
<kbabbitt> ... though I feel weird when bikeshedding the details
<kbabbitt> ... core problem is how we're going to change this
<kbabbitt> ... what would syntax look like without combinator
<kbabbitt> ... can't change how ?? behaves
<emilio> q+
<kbabbitt> s/??/::slotted/
<kbabbitt> ... and I hope we don't introduce some weird word or parameter after it
<kbabbitt> ... we have another issue for this meeding for allowing complex selectors in slotted
<kbabbitt> ... this could turn in to a combinator piece by piece
<astearns> ack miriam
<lea> s/this could turn in to a combinator piece by piece/so we're basically turning it into a combinator piece by piece, without the benefits that come with that/
<kbabbitt> miriam: to build on what emilio was saying
<kbabbitt> ... if we had any order either the component styles are firsty or they're lad
<kbabbitt> ..they'd compete not only in specificity but in layers
<lea> +1 to what miriam is saying, great point
<kbabbitt> ... [missed] that might be right
<kbabbitt> .... maybe there's an at-rule that relies on what component is doing, what page is doing
<kbabbitt> ... like emilio. curious about defining problem better
<astearns> ack emilio
<kbabbitt> emilio: wanted to emphasize defining what we need
<kbabbitt> ... constrains solution space
<kbabbitt> ... if we only care about competing with unlayered styles outside scope, a simple at-rule or some other opt in might do
<kbabbitt> ... if we need to make layers cross trees somehow, that's a whole different problem
<kbabbitt> ... I think we need to ... some sort of grouping at-rules feels like most flexible solution
<kbabbitt> ... also don't want to fixate on a solution without knowing everything we need to solve
<lea> q?
<kbabbitt> astearns: I think I'm hearing that people would like to define the problem we're trying to solve
<kbabbitt> ... not sure, is the basic problem how shadow dom styles can participate in ordering to work with light dom styles?
<kbabbitt> ... is it an ordering issue, finding a way of defining how some shadow dom styles can be ordered with outer page styles?
<kbabbitt> lea: problem is not order; it behaves likle different origin entirely
<kbabbitt> miriam: letting them interact as though in same context
<lea> btw the issue I mentioned: https://github.com/w3c/csswg-drafts/issues/7922
<kbabbitt> astearns: should we take back to issue with resolution that we want to figure out how to let these separate sets of styles interact?
<kbabbitt> ...before working on syntax solution?
<kbabbitt> ... trying to suggest something we can do for this issue, would it be better to leave this one be for now?
<kbabbitt> lea: wonder if there's web compat research we can do?
<kbabbitt> ... definitely won't be compatible
<kbabbitt> emilio: 90% sure that if I make a change and try to start Firefox, it won't look like Firefox
<kbabbitt> astearns: suggestions for how we should proceed?
<kbabbitt> emilio: think it might be useful to have a more focused issue on defining exactly what we need
<kbabbitt> ... this discussion has gone on in that issue for a while
<kbabbitt> ... not sure that would be in practice vs continuing in this issue
<kbabbitt> miriam: there are several related issues here
<kbabbitt> ... maybe there's a meta issue that needs to nail down the problem instead of individual issues
<kbabbitt> lea: I think that makes a lot of sense
<kbabbitt> ... need to look at all issues around targeting shadow dom styles together
<kbabbitt> ... I could take an action item for meta issue
<kbabbitt> astearns: let's leave this issue's discussion there
<kbabbitt> ACTION: lea to create a meta issue on shadow DOM styling and what needs to be solved
<kbabbitt> lea: to discuss in next telcon or f2f?
<kbabbitt> astearns: don't know that a deadline is nececessary
<kbabbitt> ... when we come to sort of consensus in that issue, it will be in next agenda
<kbabbitt> ... we could set a deadline but I've never seen them work in CSSWG