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

[css-scoping-1] Specificity of :host, ::slotted, and :host-context doesn't seem to be defined? #1915

Closed
emilio opened this issue Oct 28, 2017 · 39 comments

Comments

@emilio
Copy link
Collaborator

emilio commented Oct 28, 2017

Which is important for cascade order.

I think there are similar issues with other selectors like :matches and such, but those seem to have been resolved (I think?) and now matches is defined as:

The specificity of a :matches() pseudo-class is replaced by the specificity of its selector list argument. (The full selector’s specificity is equivalent to expanding out all the combinations in full, without :matches().)

Anyway. Blink calculates it dynamically based on the biggest specificity of the compound selectors that match. But as far as I can tell there's no spec that defines this (sorry if I overlooked it).

There's a problem with calculating specificity dynamically, which is that doing it converts the specificity not in just a function of the selector, but also in a function of the element, which is unfortunate (specially since that affects style sharing, since you can't assume that proving you match all the same selectors you have the same style, you also need to prove you match the exact same sub-selectors).

Also, doing it in a way that is not inconsistent requires matching through all the selectors, instead of just one, which isn't particularly great, but also not a big deal I suppose.

@Loirooriol
Copy link
Contributor

Related: #1027 and #1170

@tabatkins tabatkins added the css-scoping-1 Current Work label Nov 2, 2017
@tabatkins
Copy link
Member

My thought was just that it would have the standard specificity of a pseudo-class, and the contained selector wouldn't matter; that's why nothing else is specified, so it's covered by the Selectors default.

@emilio
Copy link
Collaborator Author

emilio commented Nov 15, 2017

My thought was just that it would have the standard specificity of a pseudo-class, and the contained selector wouldn't matter; that's why nothing else is specified, so it's covered by the Selectors default.

That definitely works for me, but it's not what Blink implements, nor WebKit, cc @rniwa

@emilio
Copy link
Collaborator Author

emilio commented Dec 13, 2017

@lilles do you have any opinion here?

@lilles
Copy link
Member

lilles commented Dec 14, 2017

Having the same specificity for the two rules below sounds unfortunate to me as I can imagine you'd want to do host overrides on classes/attributes on the host element, but I don't know to what degree it's used in practice:

:host(.pink) { color: pink }
:host { color: green }

@emilio emilio changed the title [css-scoping-1] Specificity of :host and :host-context doesn't seem to be defined? [css-scoping-1] Specificity of :host, ::slotted, and :host-context doesn't seem to be defined? Apr 9, 2018
@notwaldorf
Copy link

notwaldorf commented Apr 10, 2018

I have seen pairs of :host {...} and :host{.pink} selectors for custom elements for theming apps, so I definitely think that's a case to consider.

@emilio
Copy link
Collaborator Author

emilio commented Apr 10, 2018

Sure. This is only relevant if :host came later of :host(.pink), but I agree. I just want to get this spec'd. Presumably this can be resolved at the same time as #2271, and ideally with #2158, to avoid going back to eternal discussion like #1027.

@css-meeting-bot
Copy link
Member

The Working Group just discussed Specificity of :host, ::slotted, and :host-context doesn't seem to be defined?, and agreed to the following resolutions:

  • RESOLVED: Account for the specificity of the selector item and change the spec to say pseudo elements have same specificity of pseudo classes.
The full IRC log of that discussion <dael> Topic: Specificity of :host, ::slotted, and :host-context doesn't seem to be defined?
<dael> github: https://github.com//issues/1915
<dael> emilio: Also if we want to keep it. Per spec this has spec of normal pseudo class, but there's a request to effect specificity of selector. I Impl the spec, but blink and webkit don't touch slotted....whatever we decide I'm fine.
<dael> TabAtkins: Should be consistent.
<dael> Rossen: Anyone have a preference?
<dael> rune_: I can't see why we would do it differently
<dael> dbaron: pseudo class vs pseudo element?
<dael> TabAtkins: No, a selector with a pseudo eleemnt add specififity.
<dael> dbaron: Match different things.
<dael> emilio: slotted does that.
<dael> dbaron: Add pseudo elements to specificity
<dael> TabAtkins: Like pseudo class when you can opt in to specila specificity
<dael> TabAtkins: Should change selectors to ask spec of pseudo element defaulting to none at all.
<dael> TabAtkins: Servo asked for it explicity with a similar example.
<TabAtkins> https://github.com//issues/2271
<dael> TabAtkins: I'm okay resolving that we make all these psuedo classes have the specificity of their elements and clarify pseudo elements can have the specificity.
<dael> dbaron: The reason pseudo elements don't have specificity if you have a :before nothing before cna matc so there's no point in changing specificity when you have exactly one. You can change pseudo elements to have a class level specificity.
<dael> TabAtkins: Seems fine.
<dael> Rossen: Prop:
<dael> emilio: Accounting for the specificty of the selector item and change the spec to say pseudo elements have same specificy of pseudo classes.
<dael> emilio: If you have slotted div the spec would be the class?
<dbaron> Interesting, CSS1 said pseudo-elements count as *elements*: https://www.w3.org/TR/CSS1/#cascading-order
<dael> TabAtkins: It would be the same.
<dael> emilio: Override it.
<dael> Rossen: Objections?
<dael> RESOLVED: Account for the specificity of the selector item and change the spec to say pseudo elements have same specificity of pseudo classes.

moz-wptsync-bot pushed a commit to web-platform-tests/wpt that referenced this issue Jun 29, 2018
…city.

As resolved in w3c/csswg-drafts#1915.

Differential Revision: https://phabricator.services.mozilla.com/D1849

bugzilla-url: https://bugzilla.mozilla.org/show_bug.cgi?id=1454165
gecko-commit: ca14c36e4280927a10e43e95b8f459e767fc8178
gecko-integration-branch: autoland
gecko-reviewers: xidorn
moz-v2v-gh pushed a commit to mozilla/gecko-dev that referenced this issue Jun 29, 2018
…ctor's specificity. r=xidorn

As resolved in w3c/csswg-drafts#1915.

Differential Revision: https://phabricator.services.mozilla.com/D1849

--HG--
extra : moz-landing-system : lando
emilio added a commit to emilio/servo that referenced this issue Jun 30, 2018
xeonchen pushed a commit to xeonchen/gecko-cinnabar that referenced this issue Jul 2, 2018
moz-wptsync-bot pushed a commit to web-platform-tests/wpt that referenced this issue Jul 2, 2018
…city.

As resolved in w3c/csswg-drafts#1915.

Differential Revision: https://phabricator.services.mozilla.com/D1849

bugzilla-url: https://bugzilla.mozilla.org/show_bug.cgi?id=1454165
gecko-commit: ca14c36e4280927a10e43e95b8f459e767fc8178
gecko-integration-branch: autoland
gecko-reviewers: xidorn
@emilio
Copy link
Collaborator Author

emilio commented Sep 21, 2018

@lilles could we get this fixed in Blink? (I can submit a patch for it if you want).

We already got a report due to this behavior change in Gecko not behaving as Blink: https://bugzilla.mozilla.org/show_bug.cgi?id=1492071

@emilio
Copy link
Collaborator Author

emilio commented Sep 21, 2018

Oh, the incompatibility is more subtle:

<!doctype html>
<div id="host"></div>
<script>
  host.attachShadow({ mode: "open" }).innerHTML = `
    <style>
      :host div[foo] {
        width: 100px;
        height: 100px;
        background: green;
      }
      div[foo] {
        background: red;
      }
    </style>
    <div foo></div>
  `;
</script>

Gecko shows green because it accounts the :host pseudo-element specificity plus the inner selector's specificity. Blink only seems to account for the inner selector's specificity only.

Is this expected? I'd expect the specificity to be added, and thus :host div be a more specific selector than div.

@emilio
Copy link
Collaborator Author

emilio commented Sep 21, 2018

Turns out I had reported this to blink before... https://bugs.chromium.org/p/chromium/issues/detail?id=857415

@rniwa
Copy link

rniwa commented Sep 22, 2018

FWIW, WebKit also renders it green.

@emilio
Copy link
Collaborator Author

emilio commented Sep 22, 2018

Yeah, I gave up and fixed it on Blink, before it keeps biting us.

@lilles
Copy link
Member

lilles commented Sep 24, 2018

Regarding inner specificity, this is what's specified for :matches() and :not(). I could not find a spec'ed specificity for :host().

@lilles
Copy link
Member

lilles commented Sep 24, 2018

What's special about :host is that it's both functional and a single pseudo class, so not having the same specificity for the selectors below would be strange?

:host(*) div {}
:host div {}

@emilio
Copy link
Collaborator Author

emilio commented Sep 24, 2018

I think it would though, wouldn't it? The specificity of * is zero.

@emilio
Copy link
Collaborator Author

emilio commented Sep 24, 2018

In any case, I agree this is inconsistent with :matches and :not... I guess we need another discussion about this? I think in this case it makes sense to account for the :host specificity as well because :host is (unlike :not / :matches) selecting something, instead of just applying a union or negation operation to another selector.

So it makes more sense IMO to treat it as div:host would be treated than as :matches(div) is.

@lilles
Copy link
Member

lilles commented Sep 24, 2018

I'm fine with whatever includes the specificity of the sub-selectors, but I think we need to explicitly specify it in the spec. The non-functional :host follows from pseudo class specificity, but :host() doesn't, I think.

@emilio emilio added the Agenda+ label Sep 24, 2018
@lilles
Copy link
Member

lilles commented Sep 24, 2018

Yes. There is also ::slotted() which is a pseudo element. Is there a separate issue for that one?

@emilio
Copy link
Collaborator Author

emilio commented Sep 24, 2018

I don't think so, guess we can discuss it here as well. Though I think ::slotted doesn't really matter because its specificity IIRC can't conflict with something that isn't another ::slotted selector.

@lilles
Copy link
Member

lilles commented Sep 24, 2018

That is true.

Whether we add a pseudo element specificity for ::slotted() doesn't matter, but the spec needs to say that the selector inside ::slotted() contributes to specificity.

@smfr
Copy link
Contributor

smfr commented Sep 26, 2018

@rniwa any opinion?

@css-meeting-bot
Copy link
Member

The CSS Working Group just discussed Specificity of :host, ::slotted, and :host-context doesn't seem to be defined?, and agreed to the following:

  • RESOLVED: Specificity of :host() is 1 pseudoclass + specificity of argument. (Not just specificity of argument.)
The full IRC log of that discussion <dael> Topic: Specificity of :host, ::slotted, and :host-context doesn't seem to be defined?
<dael> github: https://github.com//issues/1915
<dael> emilio: We resolved on :host and ::slotted for specificity of inner selector. Should :host account for own specificity? There's incompat. Should :host* be same as :host-context*?
<emilio> s/:host-context*/:host(*)
<dael> Rossen_: Does anyone have an opinion?
<fantasai> Quesiton was whether :host(*) should be the same as :host
<dael> emilio: Should specificity of functiona host selector be only the selector or selector+pseudo class for specificity. I think 2nd. That is consistent and increases specificity of the selector.
<fantasai> s/:host/*:host/
<dael> TabAtkins: This is inconsistent with :matches, but host on its own just making a * argument should be 0 makes me lean toward your argument.
<dael> emilio: Works great
<dael> Rossen_: Other opinions?
<bkardell_> that's a new precident right?
<fantasai> fantasai: +1 from me
<dael> Rossen_: Objections?
<bkardell_> nothing else works like that I think?
<TabAtkins> bkardell_: Yeah, new precedent, kinda sorta.
<dael> bkardell_: It's the only decision that makes sense, but it's new. NOthing else works this way today.
<dael> emilio: Yeah, don't have many psuedo classes with selector arguments.
<bkardell_> +1
<bkardell_> it makes sense
<dael> Rossen_: Makes sense as a pattern going forward.
<TabAtkins> I mean, :matches()/:not() mean *absolutely nothing* absent their selector; there's nothing there otherwise. :host *does* affirmatively select an element all by itself, and then the selector narrows it down.
<TabAtkins> Proposed resolution: specificity of :host() is 1 pseudoclass + specificity of argument. (Not just specificity of argument.)
<dael> RESOLVED: Specificity of :host() is 1 pseudoclass + specificity of argument. (Not just specificity of argument.)

@css-meeting-bot
Copy link
Member

The CSS Working Group just discussed :host-context.

The full IRC log of that discussion <emilio> Topic: :host-context
<emilio> github: https://github.com//issues/1915
<fantasai> emilio: When looking into this stuff, what I didn't implement which Apple also doesn't, is because it's really slow
<fantasai> emilio: Reason why it's really slow is that for every class change, you either need to store everything for the whoel docuemnt, or you need to look at every shadow root that has a host context selector, and go through all the shadow roots inside your subtree to check if they have a relevant host-context rule...
<fantasai> emilio: Handling DOM changes when host-context selectors are involved
<fantasai> emilio: Blink solves this by doing subtree restyles?
<fantasai> futhark: So in Blink we aggregate style info at the docuemnt scope for everything
<fantasai> futhark: we don't use subtree calc
<fantasai> futhark: We invalidate inside the shadow root
<fantasai> futhark: We use a flag for it
<fantasai> emilio: One of the nice things in our impl is that the style info inside the shadow root is contained
<fantasai> emilio: Blink wants to make such a change, too
<fantasai> emilio: So effectively it's similarly slow as /deep/
<fantasai> emilio: Since I haven't seen anything in Bugzilla requesting it
<fantasai> emilio: And WebKit also hasn't implemented
<fantasai> emilio: So would like to drop
<fantasai> futhark: I don't have the usage numbers...
<fantasai> TabAtkins: We should see if it's used
<fantasai> TabAtkins: It offers a useful functionality but if ppl aren't suing it, I"m fine with dropping it
<fantasai> Rossen: So, collect data, come back and based on this we can move forward
<fantasai> fantasai: So maybe flag for next F2F so we don't forget
<fantasai> Rossen: Hope to get to it before

@bathos
Copy link
Contributor

bathos commented Sep 25, 2019

In Chrome, using adoptedStyleSheets for what I would expect is a core use case — a common reset sheet used by various elements with shadow roots — I was surprised to find that if the common reset sheet has e.g. * { foo: bar }, then no ::slotted(...) rule can override foo for any element, except through use of !important.

Is that consistent with the currently specified behavior, or is this a Chrome-specific issue? It’s not clear to me from reading this thread what the decision was.

@lkraav
Copy link

lkraav commented Sep 26, 2019

Is that consistent with the currently specified behavior, or is this a Chrome-specific issue? It’s not clear to me from reading this thread what the decision was.

Yep, global styles are higher specificity than ::slotted()

@lilles
Copy link
Member

lilles commented Sep 26, 2019

Is that consistent with the currently specified behavior, or is this a Chrome-specific issue? It’s not clear to me from reading this thread what the decision was.

Yep, global styles are higher specificity than ::slotted()

Strictly, specificity is not the correct term here, it's about the cascading order and the cascade criteria added here:

https://drafts.csswg.org/css-scoping/#shadow-cascading

* {} and ::slotted() {} are never compared wrt specificity. If they are in the same shadow tree, they will never match the same elements, and if they are in different trees, the mentioned cascading order will take precedence over specificity and !important is the only way to make the other rule apply.

@bathos
Copy link
Contributor

bathos commented Sep 26, 2019

Yep, I was talking about scenarios where the slot content belongs to one shadow (with the reset adopted) and the slot (and the use of ::slotted()) belong to another (which also has the reset adopted, though that doesn’t contribute to the effect).

This behavior seems to mean a lot of what would normally go in a generic, shared reset sheet isn’t viable with custom elements unless one can say for sure that they don’t and will never need to (for example) set a border color on a slotted input element or a font-weight on a slotted h2.

However I see now that this isn’t the same as the issue this thread concerns. Thanks for clearing it up.

@ExE-Boss
Copy link
Contributor

See also: #3995

gecko-dev-updater pushed a commit to marco-c/gecko-dev-comments-removed that referenced this issue Oct 3, 2019
…ctor's specificity. r=xidorn

As resolved in w3c/csswg-drafts#1915.

Differential Revision: https://phabricator.services.mozilla.com/D1849

UltraBlame original commit: ca14c36e4280927a10e43e95b8f459e767fc8178
gecko-dev-updater pushed a commit to marco-c/gecko-dev-wordified-and-comments-removed that referenced this issue Oct 3, 2019
…ctor's specificity. r=xidorn

As resolved in w3c/csswg-drafts#1915.

Differential Revision: https://phabricator.services.mozilla.com/D1849

UltraBlame original commit: ca14c36e4280927a10e43e95b8f459e767fc8178
gecko-dev-updater pushed a commit to marco-c/gecko-dev-wordified that referenced this issue Oct 3, 2019
…ctor's specificity. r=xidorn

As resolved in w3c/csswg-drafts#1915.

Differential Revision: https://phabricator.services.mozilla.com/D1849

UltraBlame original commit: ca14c36e4280927a10e43e95b8f459e767fc8178
@jorgecasar
Copy link

@lilles how can solve this situation?

<style>
/* extract from reset.css */
h1 {
  margin: 0;
  padding: 0;
  border: 0;
  font-size: 100%;
  font: inherit;
  vertical-align: baseline;
}
</style>
<fancy-heading>
  # shadow-dom
   <style>
      ::slotted(h1) {
        font-size 2rem;
        margin-bottom: 1rem;
      }
  </style>
  #/ shadow-dom
  <h1>My title</h1>
</fancy-heading>

You can't override that from the fancy-heading Web Component. Both are applying to the same element in the tree and always won the specificity war the external stylesheet.

I guess that should be a way to win this war from the web component to avoid side effects. At least, ::slotted(h1) should win

Full example here:

https://webcomponents.dev/edit/eQFJPlOvQMIuUZj71ohV/README.md

@emilio
Copy link
Collaborator Author

emilio commented Jul 19, 2021

If you want to override it from the component, that's what !important is for.

@jorgecasar
Copy link

Then I will have mi components styles plenty of !important to ensure the consistency of my Design System.

And If I open muy styles with ::parts() the user should use !important to override the values again.

I had to move all elements from light DOM to shadow DOM to ensure styles are applied without external surprises because I don't want to have my code plenty of !important.

I guess it was not well resolved the style management of the light DOM. Or at least the spec is not clear and all devs I ask thought that ::slotted(h1) should win the h1 in global styles.

I hope we can define other way in the future to improve de Developer Experience.

@jorenbroekema
Copy link

jorenbroekema commented Jul 20, 2021

That indeed makes for some truly terrible developer experience for custom elements using ShadowDOM, as it would require me to rewrite all my styles that use ::slotted to have an !important out of precaution, to prevent that global stylesheets overrides it.

Edit: For the record, yes I do believe global stylesheets should be able to override the styles as result from ::slotted, slottables are just LightDOM after all, but only if those selectors have higher specificity than the specificity of the ::slotted selector.

Use case here, taken from an accordion webcomponent https://lion-web.netlify.app/components/content/accordion/overview/ , where accordion content and invoker is passed as content projection through slots:

.accordion ::slotted([slot='invoker']) {
  margin: 0;
}

.accordion ::slotted([slot='invoker'][expanded]) {
  font-weight: bold;
}

.accordion ::slotted([slot='content']) {
  margin: 0;
  visibility: hidden;
  display: none;
}

.accordion ::slotted([slot='content'][expanded]) {
  visibility: visible;
  display: block;
}

becomes:

.accordion ::slotted([slot='invoker']) {
  margin: 0 !important;
}

.accordion ::slotted([slot='invoker'][expanded]) {
  font-weight: bold !important;
}

.accordion ::slotted([slot='content']) {
  margin: 0 !important;
  visibility: hidden !important;
  display: none !important;
}

.accordion ::slotted([slot='content'][expanded]) {
  visibility: visible !important;
  display: block !important;
}

That also means that if I have a web component extension class that wants to extend my component with overrides, they themselves will be forced to do the same thing as well.

I would definitely prefer it if:

Specificity of ::slotted() is 1 pseudoclass + specificity of argument.

Similar to :host, for pretty much the same reasons.

@jorgecasar
Copy link

@jorenbroekema other way is to use custom properties everywhere because I just tested, and once you put !important in your component, there is no way to override it from outside.

It's really painful if you want to build a Design System using the light DOM. It seems standard is forcing us to put all the content in the shadow DOM, with its problems. Then we will need declarative Shadow DOM, SSR, etc...

@jorenbroekema
Copy link

once you put !important in your component, there is no way to override it from outside.

Yeah that's a big downside since you want your users to do styling overrides on LightDOM, as this is basically their content, their territory..

So correct me if I'm wrong but you have to choose between:

  • Stuff outside always overrides your slotted selector (default behavior)
  • Stuff outside cannot override your slotted selector (if you use !important inside your slotted selectors)
  • Custom CSS props with fallbacks as default value, everywhere 🤷 :
.accordion ::slotted([slot='invoker']) {
  margin: var(--accordion-invoker-margin, 0);
}

@emilio
Copy link
Collaborator Author

emilio commented Jul 20, 2021

I mean, the way !important was designed to work on shadow DOM was to, by default, allow the "outside" to style its stuff as it wants, but let the component use !important for stuff it relies on.

For the example of the accordion, it probably makes sense that, given hiding stuff is core to it, display: none / visibility: hidden should probably be !important. I see no reason why font-weight / margin / etc should be !important there unless I'm missing something though.

Anyways, it seems to me like if the current model doesn't solve some of the use-cases, those should probably be discussed in a separate new issue, rather than a 4 year old closed issue.

@jorgecasar
Copy link

jorgecasar commented Jul 20, 2021

Here other example. You have some generic styles for headings, paragraph, etc.. to ensure your web looks fine.

I wants component called hero for the home page or a card to list products. The will have a heading inside. I could style this elements because of the generic selector styles.

<fancy-hero>
  <h1>My site is awesome</h1>
  <h2>It use Web Components</h2>
</fancy-hero>

<h2>My products</h2>

<ul>
  <li>
    <fancy-card>
      <h3>Product one</h3>
      <p>A really cool product description</p>
    </fancy-card>
  </li>
</ul>

Currently, you have to put important in all styles of your component that apply to avoid being apply to the elements inside your components.

If you try to upgrade a legacy project with tons of css your components are very fragile. And !important solution gets very bad press among developers.

PD: I will create a new issue to discuss this use case

@jorenbroekema
Copy link

jorenbroekema commented Dec 15, 2021

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.

Edit: sorry I meant to write this on the more recent open issue, didn't mean to revive this closed one :(

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests