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

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

Open
bramus opened this issue Jun 25, 2024 · 11 comments
Open

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

bramus opened this issue Jun 25, 2024 · 11 comments
Labels
selectors-4 Current Work

Comments

@bramus
Copy link
Contributor

bramus commented Jun 25, 2024

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 bramus added the selectors-4 Current Work label Jun 25, 2024
@Th3S4mur41
Copy link

Thanks @bramus for following up on this 👍

@tabatkins
Copy link
Member

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
Copy link
Member

lukewarlow commented Jun 25, 2024

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
Copy link
Member

tabatkins commented Jun 25, 2024

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
Copy link
Member

tabatkins commented Jun 25, 2024

@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
Copy link
Member

: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
Copy link
Member

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

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

@mayank99
Copy link

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

@Th3S4mur41
Copy link

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

@bramus
Copy link
Contributor Author

bramus commented Jul 1, 2024

(#) 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)

@mayank99
Copy link

mayank99 commented Jul 1, 2024

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
selectors-4 Current Work
Projects
None yet
Development

No branches or pull requests

5 participants