w3c / csswg-drafts

CSS Working Group Editor Drafts
https://drafts.csswg.org/
Other
4.35k stars 639 forks source link

[selectors] Behavior of :root inside Shadow DOM #10492

Open bramus opened 4 days ago

bramus commented 4 days ago

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.

Th3S4mur41 commented 4 days ago

Thanks @bramus for following up on this 👍

tabatkins commented 3 days ago

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?

lukewarlow commented 3 days ago

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.

tabatkins commented 3 days ago

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 commented 3 days ago

@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.

lukewarlow commented 3 days ago

: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?

tabatkins commented 3 days ago

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

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

mayank99 commented 3 days ago

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).