csswg-drafts icon indicating copy to clipboard operation
csswg-drafts copied to clipboard

[selectors] Behavior of :root inside Shadow DOM

Open bramus opened this issue 1 year ago • 11 comments
trafficstars

The short version

Using :root in Shadow DOM currently does not match anything. Sparked by https://x.com/Th3S4mur41/status/1805198579910238380 I think it would make sense to have that match the Shadow Root.

One wouldn’t be able to use it to style the shadow with it, but it would enable use-cases such as :has(:popover-open) because that selector would then also match :root:has(:popover-open) – similar to how it behaves in Light DOM.

It would fix situations such as this one.

The long version

In https://x.com/Th3S4mur41/status/1805198579910238380 an author wondered why the following worked in Light DOM but not in Shadow DOM as seen in this demo:

/* Change the popover invoker style when open */
:has([popover]:popover-open) [popovertarget="mypopover"] {
	background: yellow;
}

Note that in the Shadow DOM variant the popover itself is also part of the Shadow DOM:

<div id="shadow" class="container">
  ↳ #shadow-root
      <button popovertarget="shadowpopover">button in shadow DOM</button>
      <div id="shadowpopover" popover="">popover in shadow DOM</div>
</div>

Reducing the code it’s the :has(:popover-open) that is not working as the author expected. This because with an open popover, :has(:popover-open) can only match the #shadow-root, which itself is not selectable.

This gave me the idea to have :root match the #shadow-root. One wouldn’t be able to style anything with it, but it would make the use-case above work because :has(:popover-open) – which is *:has(:popover-open) – would then be able to match the shadow-root.

/cc @keithamus and @lukewarlow who participated in the discussion on X.

bramus avatar Jun 25 '24 07:06 bramus

Thanks @bramus for following up on this 👍

Th3S4mur41 avatar Jun 25 '24 11:06 Th3S4mur41

There's already a selector that matches the shadow host - :host. The shadow root isn't an element; it's a DocumentFragment, and doesn't reflect into CSS's tree.

I'm a little confused by the use-case, tho - how would this help :has([popover]:popover-open) [popovertarget="mypopover"] match? :root isn't mentioned in there at all. Is the first compound meant to match the host element?

tabatkins avatar Jun 25 '24 21:06 tabatkins

The use case is to not need a single root element in shadow roots to use :has() in this case the root has two root elements, a button and a popover. With the right markup you could get next sibling combinators working, but it'd be nice if you could use :has() instead. The :root idea is possibly you could do :root:has() or something.

lukewarlow avatar Jun 25 '24 21:06 lukewarlow

In the linked tweet (and the codepen it links to), the reason it doesnt match is because the selector is in the light dom, and selectors never see into shadows (except for the couple of special-case things like ::part and ::slotted). That definitely wouldn't be fixed by anything we do about :root matching.

Nm, I misread the codepen, it duplicates that style in the shadow dom constructor.

tabatkins avatar Jun 25 '24 21:06 tabatkins

@lukewarlow That's what :host is for, yes. Within the shadow, the host element is treated as the root of the shadow tree, so :host(:has(...)) [popover] should work just fine.

tabatkins avatar Jun 25 '24 21:06 tabatkins

:host:has(:popover-open) button {background: yellow;} doesn't set the buttons background to yellow. I can only assume :host:has() counts as shadow piercing and so doesn't work?

lukewarlow avatar Jun 25 '24 21:06 lukewarlow

Sorry, I corrected my selector via a comment edit. ^_^

(But it still doesn't work, which I think is a bug.)

tabatkins avatar Jun 25 '24 21:06 tabatkins

I don't think :root should match the shadow host. Currently, :root is the only selector that never matches anything inside shadow DOM. Today, I can use this knowledge to write @scope (:root) rules which don't get applied to shadow elements, even if the stylesheet gets adopted/linked in the shadow DOM. Similarly, I can write isomorphic stylesheets using @scope (:root, :host). In both of these cases, I can use :scope to refer to the scope root, whether that's :root or :host.

The original problem can be solved using a selector like [popovertarget="shadowpopover"]:has(+ :popover-open).

mayank99 avatar Jun 26 '24 02:06 mayank99

Wouldn't a dedicated :shadowroot selector make sense in that case?

Apart for the described use cases, there are most definitely use cases for the :root element in the light the DOM, the same use cases would probably apply to a similar element in the shadow DOM.

e.g.: It would make sense to define "private" custom properties for a web component in the root of the shadow DOM rather in than in the :host. This currently requires adding a div wrapper inside the web component

Th3S4mur41 avatar Jul 01 '24 08:07 Th3S4mur41

(#) I'm a little confused by the use-case, tho - how would this help :has([popover]:popover-open) [popovertarget="mypopover"] match? :root isn't mentioned in there at all.

:has([popover]:popover-open) has an implicit universal selector prepended to it. Problem here is that * doesn’t match the shadow root itself (which, in itself, makes sense). Therefore authors can’t copy over code from Light do Shadow DOM without changing the selectors.

The suggestion was to have :root match the Shadow Root. That way the * selector would also match that Shadow Root, resulting in :has([popover]:popover-open) / :root:has([popover]:popover-open) work in both Light and Shadow DOM.

(Admittedly authors should write :root:has([popover]:popover-open) instead of :has([popover]:popover-open) for performance reasons)

(#) Currently, :root is the only selector that never matches anything inside shadow DOM. Today, I can use this knowledge to write @scope (:root) rules which don't get applied to shadow elements, even if the stylesheet gets adopted/linked in the shadow DOM.

If :root also were to match the Shadow Root, you would not lose this ability as you can then achieve the same with :host :root.

(#) The original problem can be solved using a selector like [popovertarget="shadowpopover"]:has(+ :popover-open).

I don’t think we should force authors to rewrite their stylesheets when using them inside a shadow root or not. I would expect :root:has([popover]:popover-open) to work in both cases. That way they can import and adopt stylesheets in Light and Shadow DOM.

(#) Wouldn't a dedicated :shadowroot selector make sense in that case?

That would also make the original use-case work, but would require authors to write :is(:root, :shadowroot):has([popover]:popover-open) to make their stylesheets portable while also writing a more performant selector than simply :has([popover]:popover-open)

bramus avatar Jul 01 '24 12:07 bramus

@bramus

I would expect :root:has([popover]:popover-open) to work in both cases.

That's the thing, I would not expect this to work. :root is effectively the same as html, which does not exist inside shadow DOM.

I don’t think we should force authors to rewrite their stylesheets when using them inside a shadow root or not.

Ironically, with this change, you would be forcing authors who rely on the current :root behavior to rewrite their stylesheets. For example, someone might have written something like :root { height: 100% } knowing that it only applies to <html> and not shadow-roots.


I think it's worth focusing on @tabatkins and @lukewarlow's discussion for a second.

:host refers to the host element in light DOM. :host(:has()) wouldn't check for the presence of elements inside the shadow-tree. It would look only in the light tree, because that's where the element is.

For example, this works in Firefox and Safari today (but not in Chrome?):

<div>
  <template shadowrootmode="open">
    <style>
      :host(:has(p)) { color: red; }
    </style>
    <slot></slot>
  </template>

  <p>Red in the light DOM</p>
</div>

@Th3S4mur41 I should point out that a :shadowroot-like selector is also being discussed to allow styling shadow-tree elements from the outside. I imagine it would it would be used like my-component:shadowroot(div). Maybe the two ideas would conflict, or maybe they could be made to work together.

mayank99 avatar Jul 01 '24 14:07 mayank99

In https://github.com/w3c/csswg-drafts/issues/11000#issuecomment-2622719814, it was resolved to "have :scope resolve to :host rather than :root in shadow tree".

Can this issue be closed?

mayank99 avatar Jan 30 '25 17:01 mayank99

Yes.

bramus avatar Jan 30 '25 22:01 bramus