Open jorgecasar opened 3 years ago
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.
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.
Description
According to the spec 3.2.4. Selecting Slot-Assigned Content: the ::slotted() pseudo-element:
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 andh1
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.
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 withoutfancy-hero
wrapper, we have some generic styles like this:Then the color of my
fancy-hero h1
changed to#333
. The only way to preserve thefancy-hero h1
style from inside thefancy-hero
is by applying!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:
Then I can customize my
fancy-hero
adding this styles into the global styles: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:
!important
and 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 andh1
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