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

[specificity] Specifity of ::slotted() is greater than :host #2290

Closed
limitlessloop opened this issue Feb 8, 2018 · 15 comments

Comments

@limitlessloop
Copy link

@limitlessloop limitlessloop commented Feb 8, 2018

If you declare styles inside a component which use the :slotted() selector then styles applied by any child components which are uses inside that component cannot be overridden using :host.

/* Parent custom element */
:slotted(*) {
    color: green;
}

/* Child custom element */
:host {
    color: red;
}

I would expect the child custom element's text to be red, but it is in fact green.

http://jsbin.com/cecuxad/edit?html,console,output

Is there anything in the spec that covers this behaviour?

@emilio

This comment has been minimized.

Copy link
Collaborator

@emilio emilio commented Apr 14, 2018

Unless I'm missing something this is working as expected. The children would only be red because of inheritance, but there's a rule specifying its color to green.

@emilio

This comment has been minimized.

Copy link
Collaborator

@emilio emilio commented Apr 14, 2018

i.e., this doesn't have to do anything with specificity at all.

@tabatkins

This comment has been minimized.

Copy link
Member

@tabatkins tabatkins commented Apr 16, 2018

Yes, this particular issue is purely a result of the way the cascade handles inheritance vs direct application; it has nothing to do with specificity at all, since there aren't any clashing rules.

@limitlessloop

This comment has been minimized.

Copy link
Author

@limitlessloop limitlessloop commented Apr 16, 2018

Sorry it's not clear from the replies, is it an issue or not? If it's not specificity, is it just a downfall of the "direct application" you mention?

If it were standard html and css it would be red wouldn't it?

<style>
.host1 * {
    color: green;
}
.host2 {
    color: red;
}
</style>

<div class="host1">
    <div>green</div>
    <div class="host2">red</div>
</div>

This undesired behaviour causes a lot of problems when designing composable elements as the nested custom element has no way of overriding the rules governed by the parent custom element (unless there is a technique I don't know about?).

@tabatkins

This comment has been minimized.

Copy link
Member

@tabatkins tabatkins commented Apr 17, 2018

In your original example, the green and the red aren't being specified on the same element, so there's no specificity issue. Instead, you're setting the color on :host and relying on inheritance to propagate that downwards, but inheritance always loses to directly styling an element (which ::slotted(*) does), no matter what selector it comes from.

In your latest example, the two declarations are specifying the same property on a single element - "color: green" is being specified on the host2 element via the .host1 * selector, and "color: red" via the .host2 selector. Since there are competing declarations, we rely on specificity to sort things out - in this case, both selectors have specificity [0,1,0] (because the universal selector doesn't add any specificity), and so we fall back to source ordering, so "red" wins.

@limitlessloop

This comment has been minimized.

Copy link
Author

@limitlessloop limitlessloop commented Apr 17, 2018

In your original example, the green and the red aren't being specified on the same element, so there's no specificity issue.

Are they not being specified on the same element?

/* my-parent.html */
:slotted(*) {
    color: green;
}

targets the same element that

/* my-child.html */
:host {
    color: red;
}

does?

<my-parent>
    <my-child></my-child>
</my-parent>

The child element is the slotted element of the parent.

Perhaps it's confusing things because I am using an example property which is inherited? This would still be an issue if it was using margin for example.

@emilio

This comment has been minimized.

Copy link
Collaborator

@emilio emilio commented Apr 17, 2018

Oh, I see what you mean. host rules are indeed applied before slotted rules indeed, even though !important rules are applied in the reverse order (except in FF where we still don't implement that last bit, though we don't ship Shadow DOM).

@emilio

This comment has been minimized.

Copy link
Collaborator

@emilio emilio commented Apr 17, 2018

It's still not about specificity itself though.

@limitlessloop

This comment has been minimized.

Copy link
Author

@limitlessloop limitlessloop commented Apr 17, 2018

Ok it might not be about specificity, but to a layman I have no other way of describing it.

How do we take this further?

Is it a spec issue or a technical issue?

@tabatkins

This comment has been minimized.

Copy link
Member

@tabatkins tabatkins commented Apr 17, 2018

Oh! I didn't realize there was a parent/child relationship going on here, I gotcha now. I should read more. (Cases like this are often confusing to explain; it helps to provide a markup example with shadow trees drawn in.)

(Yeah, it's technically not specificity, but that's close enough to work. Technically it's the "Shadow Tree" cascade step; specificity is another cascade step.)

So yeah, the ::slotted(*) and :host rules come from different trees, and the ::slotted(*) tree is earlier in the tree order (as it's a parent), so it wins. This is meant to address the usage pattern that components can set up their defaults via normal styling, then users of the components can reach in and override easily; components can guard styles that must be a particular value with !important, which wins over outside rules.

So yeah, the element ends up green, and that's intended behavior.

In general, styling of the host element is a coordination between the shadow tree and the outside page. ::slotted() isn't relevant to this example; you could just as easily have had:

<style>
.foo { color: green; }
</style>
<x-foo class=foo>
 <::shadow>
  <style>
  :host { color: red; }
  </style>
 </::shadow>
</x-foo>

In this case, the element ends up with color: green, again because the "green" rule comes from an "earlier" tree in the tree-of-trees than the "red" rule.

@limitlessloop

This comment has been minimized.

Copy link
Author

@limitlessloop limitlessloop commented Apr 17, 2018

I did provide a jsbin example but perhaps I should have been more explicit in my explanation.

Thanks very much for taking the time to explain that. I won't pretend to understand it all but I get the gist of it. I don't agree with the approach of using !important one should allow nested components to supersede styling from up the tree, but not to prevent it entirely.

Coming from conventional html and css and my inferior experience with custom elements and the shadow dom, it doesn't seem natural to me, but I appreciate I'm probably not smart enough to understand the reasons why.

The best method I can find is to use a custom css property. This allows me to let nested custom elements override styles from outside but still allow me override the styling of the component should I need to. It might be useful to someone else in my situation although I feel one shouldn't have to do this because it can complicate things depending on your project.

/* my-parent.html */
:slotted(*) {
    color: var(--color);
    --color: green;
}

/* my-child.html */
:host {
    color: var(--color);
   --color: red !important;
}
@emilio

This comment has been minimized.

Copy link
Collaborator

@emilio emilio commented Apr 18, 2018

Yeah, it also seems a bit unnatural to me the implications this has for the style attribute. For example, this means that style="foo!important" can be overriden by an !important :host rule of its shadow host:

<!doctype html>
<div id="host" style="color: purple !important"></div>
<script>
  host.attachShadow({ mode: "open" }).innerHTML = `
    <style>:host { color: blue !important; }</style>
  `;
</script>

But I guess it's ok, I guess I can see cases where shadow trees really don't want their styles overriden by anything.

@limitlessloop

This comment has been minimized.

Copy link
Author

@limitlessloop limitlessloop commented Apr 18, 2018

I think I understand it a bit better now. I hadn't realised that the same occurs when you have an example like the following:

<style>
    h1 {
        color: green;
    }
</style>
<app-container>
    <::shadow>
    <style>
        ::slotted(*) { color: red; }
    </style>
    </::shadow>
</app-container>

The output will be a green h1. It does make sense but it means that the only way for the custom element to change the colour the h1 is to use !important but if you do that you have no way of topping this.

There needs to be a way of achieving the same as below but without having to apply the styles outside the custom element.

<style>
* {
    color: green;
}
app-container {
    color: red;
}
</style>

<!-- ... -->

<app-container>
    text will be red
</app-container>
@tabatkins

This comment has been minimized.

Copy link
Member

@tabatkins tabatkins commented Apr 18, 2018

Coming from conventional html and css and my inferior experience with custom elements and the shadow dom, it doesn't seem natural to me, but I appreciate I'm probably not smart enough to understand the reasons why.

A few reasons went into the current design:

  1. We purposely didn't involve specificity at all. Doing so would expose implementation details of the component, which makes code fragile - if the component is updated and changes the exact selector it uses, it might start overriding outside rules that previously won, or vice versa, and there's no good way for the component's user to understand or manipulate this.

  2. So we have to decide in some other way. Document order (the final cascade step) doesn't really work here - it adds an unexpected dependency on exactly how you load the custom element, and might have interesting race conditions.

  3. So we're left with Cascade Origin, or something close to it, that just unreservedly makes one or the other win. Actually injecting a new origin into the list didn't seem like a great idea; it's unclear how user vs author stylesheets should interact with this. So instead we add another cascade step for this.

  4. And finally, we have to make a decision about which wins. We already know that whatever order we choose, !important should have the reverse order; this is how the cascade origins already work. So we have to decide whether the outer page wins by default, but the component wins in !important, or the reverse. We decided that the former made more sense; this means that the component author's normal styles are "defaults", the component user's styles (!important or not) can override that, and the component author's !important styles can be used to "lock down" styles that need to stay how they are. Going the other way around didn't seem to satisfy use-cases as well: it would mean that component users can't override styles by default; they'd have to use !important to do so, possibly interfering with their other styles; and then component authors would have no way of "locking down" styles.

@limitlessloop

This comment has been minimized.

Copy link
Author

@limitlessloop limitlessloop commented Apr 19, 2018

Thanks for explaining.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
5 participants
You can’t perform that action at this time.