8000 [css-scoping] Allow elements to expose a subset of their shadow tree, which can then be styled with regular CSS · Issue #10939 · w3c/csswg-drafts · GitHub
[go: up one dir, main page]

Skip to content

[css-scoping] Allow elements to expose a subset of their shadow tree, which can then be styled with regular CSS #10939

@LeaVerou

Description

@LeaVerou

(This is a very ambitious proposal that I don’t imagine would gain much support from implementors anytime soon — however I think there’s still value in filing these "north star UIs", as we often find a way down the line)

Background

The need to expose certain shadow DOM elements to styling from the outside is well-recognized. However, the current mechanism of using ::part() pseudo-elements suffers from poor ergonomics, still doesn’t fully cover authors' use cases, and constantly brings up new design questions, such as:

Furthermore, I’ve seen many (most?) components adding part attributes on almost every element in their shadow DOM. I would go as far as to say that for most WCs I’ve seen that use parts, expose more than half of their shadow DOM elements, often more than 90%. For example, take a look at this list of parts from Shoelace’s <sl-tab-group>.

This is not only tedious for WC authors, it makes things harder for WC users as they need to learn the various part names, understand what type of element each part corresponds to and where it stands in the hierarchy and there is no way to target relationships between parts, even if every element involved is a part.

open-stylable shadow roots is one solution to this problem, but it’s an all-or-nothing solution that requires giving up encapsulation entirely.

Proposal

I’ve been wondering what could describe author intent more directly here. The intent is to keep certain elements encapsulated (e.g. wrappers) while exposing others that WC users may want to customize, ideally as a tree. What if they could do just that?

tl;dr: Authors can expose a subset of their shadow tree that can be styled from the outside with regular CSS.

MVP:

  1. An HTML attribute opts an element in to being exposed (name TBB, e.g. export).
    • If used without a value, it only shallowly exports that one element. This is the MVP.
  2. We introduce a combinator (could re-introduce >>> but with this distinct meaning rather than the previous "anything goes" semantics) that pierces into the shadow DOM, but only has access to that exposed subtree.
    • The subtree only consists of exposed elements, but within it everything works as expected, child combinators, sibling combinators, tree pseudos, you name it.
    • The matching root (i.e. the part that follows after >>>) does not need to be at the top-level, it can be anywhere on the tree. This means that my-component >>> [part=foo] is essentially ::part(foo) with better ergonomics.

Going further:

  • We may decide to make this even more granular, with values to not expose element names, classes, ids, certain attributes etc.
  • A special value (e.g. export="subtree") could export an entire subtree. If the IDL attribute is available on shadow roots, an entire shadow root can be opted in to this with 1 loc.
    • In that case, there could be a counter attribute to remove subtrees as well (donut scope).

Nice synergies:

Example

Suppose we have a <foo-spinner> with this structure and these export attributes:

<foo-spinner>
	<template shadowrootmode="open">
		<div class="wrapper">
			<input export>
			<div class="buttons">
				<button class="increment" export>+</button>
				<button class="decrement" export>-</button>
			</div>
		</div>
	</template>
</foo-spinner>

This would expose the following subtree that >>> would "see":

<foo-spinner>
	< :: exposed subtree >
		<input>
		<button class="increment">+</button>
		<button class="decrement">-</button>
	< / :: exposed subtree >
</foo-spinner>

This means that selectors like foo-spinner >>> .increment:active + .decrement actually work.
Or even foo-spinner > input:not(:blank) ~ button, even though that matches on a tree relationship that does not actually exist in the shadow tree.

Issues

  1. Because the parent-child relationships are not necessarily the same as in the shadow DOM, applying CSS properties that depend on parent-child relationships, such as flexbox and grid could have surprising results. However, ::part() also has this issue and probably any mechanism that allows exposing only a subset of the tree. One solution could be to define that exposition preserves the general shape of the tree, and non-exposed nodes simply cannot be targeted, but this seems both harder to spec, harder to implement and harder to conceptualize for authors.

Open questions

  • Do nested shadow trees need another >>> or once you have one it has access to the flattened exposed subtree?
  • Thinking about it some more, I wonder if the best approach is opting nodes out rather than opting them in, since in most use cases the number of nodes exposed is way over 50%.

Metadata

Metadata

Assignees

No one assigned

    Labels

    HTMLRequires coordination with HTML peoplecss-scoping-2

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

      0