-
Notifications
You must be signed in to change notification settings - Fork 688
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
[css-scoping] Allow elements to expose a subset of their shadow tree, which can then be styled with regular CSS #10939
Comments
Conditionally changing the tree you match selectors against is probably not great for performance (ignoring all the other issues about where do styles come from), fwiw. |
I would hesitate to call the attribute |
I definitely like where this proposal is going! The idea of creating a subtree of exposed elements/subtrees is a great idea imo. I worry that as it stands right now, this proposal might cause:
I'll provide some more details below Brittle selectorsI'll agree that being able to write complex selectors, including relationship selectors, would be hugely beneficial over todays implementation of parts. However, if component consumers write selectors that depend on other characteristics of the exported element, then those selectors are immediately brittle for changes to the component later on. Any kind of selector in the shadow root can't necessarily be depended on to always exist. A Personally speaking, I've never devoted exhaustive testing to make sure that certain css classes are on certain elements in the shadow root, unless thats the only way I have to test a public API feature. Therefore, any css selector used to style an exported element will instantly break when any of those selectors no longer exist on that exported element. Component authors will make hundreds of internal-only type changes that restructure the shadow root template without changing the public API of the component in a breaking way. IMO, a solution to theming shouldn't encourage component consumers to rely on internal structure because it makes those styles very easily breakable in a way that will be very difficult and tedious to discover and fix. Anyone that writes selectors like the above will have to examine the new contents of the shadow root of the patch-bumped component to find what changed and how in order to fix their theme styling selectors accordingly. Backwards compatibility burdenWith the This would mean that component authors would have a LOT more breaking changes to think about. ProposalI love the idea of the subtree and the attribute to denote exporting the element to create it, but I think it needs some stable identifier that CAN be a part of the public API of the component without being related to the actual shape of the dom itself. Imo the stable identifiers are part of the good part of css parts. (lol, "parts" has no meaning to me now) Currently a CSS part is just a name. Its not an element, class, or attribute. A component author can move the name around in the DOM independent of styles, attributes, IDs, or elements. For this proposal, I think it would be important to do the same. There needs to be an independent identifier associated with each exported thing. Unfortunately, I think that the issue with stable identifiers might also mean that exporting a whole subtree might not be tenable? What if the subtree was not a tree of DOM elements, but was a "part tree" constructed of part names and not elements? IMO that could be what <foo-spinner>
<template shadowrootmode="open">
<div class="wrapper" export="actions">
<input export="main-input">
<div class="buttons">
<button class="increment" export="plus-button">+</button>
<button class="decrement" export="minus-button">-</button>
</div>
</div>
</template>
</foo-spinner> <foo-spinner>
< :: exposed parts tree >
:part(main-input)
:part(plus-button)
:part(minus-button)
< / :: exposed parts tree >
</foo-spinner> and css like: my-component >>> :part(main-input) + :part(minus-button) { /* some thing */}
my-component >>> :part(actions):has(:part(minus-button)) { /* some thing */} |
I really like the direction this is going. At TPAC, I suggested that we want the old deep/shadow combinators (similar to
|
At very least, I would expect something like this would have to come from shadow styles, and not from the page. But broadly I don't think the DOM annotation is really expendable here. There's a strange circularity to using external selectors as a way of isolating parts of the DOM from external selection. That's why the |
@michaelwarren1106 I do agree that this could lead to brittle selectors, though in many cases there is very obviously a single element type that can be targeted. E.g. for the spinner example above, it is highly unlikely you’d want to change the A big pain point with parts that this was designed to solve was the burden of defining names for the component author and learning names for the component consumer, which balloons for complex use cases (e.g. think of styling an entire date picker popup). A solution with less cognitive overhead could be in line with the tree subsetting idea, by introducing higher granularity around what is exported (which I mentioned in the OP as a Level 2 feature): being able to hide certain attributes from the exported subtree or even the element type (simply by defining that no type selector can ever match it). It is an open question what should the default behavior be and what the syntax for this would be exactly. @sorvell I suspect that doing this in CSS makes it much harder to implement, (currently) impossible to monitor changes to by authors, and introduces the potential for cycles (we can design the feature in such a way that it avoids cycles, but it’s an additional design consideration). It is also unclear what the benefit is: CSS is good for applying things to elements in aggregate, but here you typically only have one instance of each element to deal with, so this seems like it introduces friction and indirection without much benefit. |
I guess my question is about the priority of the pain points. I wonder how much of a pain point learning part names would be if we could compare it to brittle selectors? As a component author and consumer, if some third-party component pushes a patch bump and breaks my code and tests without me knowing that was going to happen, that strikes me as a MUCH bigger pain the butt than reading the docs for some names? We can't compare because parts doesn't have the brittle code/test concern because of the naming feature. But I do know that shoelace/web awesome will update constantly. As a component consumer I would want component authors to be updating constantly if they can do those updates in patch bumps that are seamless and backwards compatible. Broken code and tests leads to junk GH issues ("update your code, the internals have changed"). An API that forces component authors into a choice of "if I use the If we were to try to compare the pain of establishing and learning disconnected names vs the pain of having to update your entire application because a component changed its internals in a patch bump, I would argue that the pain of the latter is much greater. I would also argue that exposing internals via names isn't actually going to do much in the way of lessening the pain of learning which pieces are exposed. Component authors are still going to have to document the exposed subtree. Component consumers are still going to have to read those docs to know which elements are available for custom styling. What difference does it really make if the string in the docs is |
A use case I would like to point out in favour of this approach, is that it enables shadow roots to contain content on behalf of their users (i.e. "include"/"import"-like elements). For example suppose we have a tooltip that shows some content from another file: <!-- index.html -->
At the event we had <j-tooltip class="cheese-nane" src="./cheese-facts/brie.html">brie</j-tooltip> ... then the shadow root might look something like: <j-tooltip src="./cheese-facts/brie.html">
::shadow-root
<div id="tooltip-target"><slot></slot></div>
<div id="tooltip-content" popover="auto">
<!-- We have an extra div with a shadowroot here to prevent cheese-facts/brie.html stylesheets affecting our shadow root -->
<div BIKESHED_EXPOSE_SUBTREE>
::shadow-root
<div BIKESHED_EXPOSE_SUBTREE>
<!-- Content from some template in cheese-facts/brie.html -->
Brie is a soft cheese ... similar to <j-tooltip class-name="cheese-name" src="./cheese-facts/camembert.html">camembert</j-tooltip>
</div>
</div>
</div>
</j-tooltip> Users could then target /* Just works? */
.cheese-name {
color: blue;
} without affecting the |
Oops, nevermind, looks like we already sort of have that through:
|
agree with this 100%. it makes more sense to me that consumers would be able to style things as normal (with all the existing selectors/combinators they're used to) unless they're explicitly declared as "private". |
(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:::part()
that is sufficient to produce interoperability #10787::details-content
vsdetails::part(content)
#9951Furthermore, 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:
export
).>>>
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.>>>
) does not need to be at the top-level, it can be anywhere on the tree. This means thatmy-component >>> [part=foo]
is essentially::part(foo)
with better ergonomics.Going further:
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.Nice synergies:
/slotted/
as a combinator (see [css-shadow-parts] Make::slotted()
a combinator #7922), it works with these subtrees out of the box, no need to introduce even more syntax about what you can have after a::part()
(see [css-shadow-parts][css-scoping] Is ::slotted() allowed after ::part()? #10807 ).Example
Suppose we have a
<foo-spinner>
with this structure and theseexport
attributes:This would expose the following subtree that
>>>
would "see":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
::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
>>>
or once you have one it has access to the flattened exposed subtree?The text was updated successfully, but these errors were encountered: