-
Notifications
You must be signed in to change notification settings - Fork 378
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
"open-stylable" Shadow Roots #909
Comments
We can't evaluate a selector across shadow boundaries because of the way we architected our engine, and we don't want to change that. |
I think there's something to be said for this idea:
Possibly a bit off topic, but StencilJS went through great lengths to support both ShadowDOM and non-ShadowDOM, using the same component / syntax. It sounds like the biggest challenge was how to emulate slots without the help of ShadowDOM. Performance is an issue with this emulation, apparently. I think userland has demonstrated that the slot concept is quite useful, even without ShadowDOM. React has something roughly similar, perhaps. I'm guessing the most natural fit for supporting this would be as part of the template instantiation initiative. I know that initiative is currently awaiting a determination whether there are underlying primitives that would benefit the platform more generally. Perhaps in parallel to that effort, template instantiation could start, but focus squarely on this (additional?) requirement, which does seem to be quite widely applicable, but much harder to implement in a performant way than other features of that proposal? Or maybe there are also some underlying primitives that would make slot emulation easier to implement / faster performing? If not, I would still suggest looking at supporting slots when/if the discussion does move on to actual declarative template instantiation. |
With adopted stylesheets, how bad would it be to apply the same stylesheet to all elements? Is there a negative performance impact even when it's same stylesheet in memory? Component libraries could offer easy mechanisms to inject those global stylesheets into the components. |
@rniwa interesting, thanks. If selectors weren't evaluated across shadow boundaries, would it be possible to run them whole inside a child shadow root? @LarsDenBakker I was wondering if an open-stylable concept could essentially be that open-stylable roots inherit the sheets from the scope above them. There are a lot of tricky problems with a userland library trying to make this work. You need mutation observers to listen for all |
@justinfagnani That idea does indeed sound tricky but assuming we have CSS modules you could easily build a Tailwind class mixin that an app team folds into their base class, no? |
@bahrus I don't think Say a component rendering to it's own light DOM, something like: <my-card>
<slot name="title">Alert</slot>
<slot></slot>
<button>OK</button>
</my-card> And then a user would like to use it: <my-element>
<h1 slot="title">Welcome</h1>
<p>Thanks for reading my card...</p>
</my-element> What should render? When? We have two timings to deal with: 1) the usual content exists before element's 2) the element's content exist before the users. In 1) The element could accidentally overwrite the user's content. Or it could append to itself. Then it would have to move the user's content into the slot, but the user wouldn't know that and could keep appending into what's now the element's semi-private DOM. In 2) If the user uses a template system like vdom, they may remove all the element's content. The only way to get this to work in any stable way is make a contract that separates user-provided children from element provided with a special child element. Then either all user provided content must go in the child, or all element provided: And then you can't really use <my-element>
<content>
<h1 slot="title">Welcome</h1>
<p>Thanks for reading my card...</p>
</content>
<content-slot name="title">Alert</content-slot>
<content-slot></content-slot>
<button>OK</button>
</my-element> This gets really complicated really quick. There's a reason I keep rejecting this feature in LitElement. |
@matthewp yeah, for any given CSS library we can probably formulate a way to make that library more compatible with shadow DOM. Component authors might not have the time, ability, or context to do so though - they might not know what context they're running in exactly - what specific style sheets are used - but they might know that the existing DOM they're replacing is styled via certain class names. This is when we hear that for some projects it's simply not feasible to use shadow DOM. |
I am wary of the all-or-nothingness of e.g. Suppose you want to use tailwind at the root level, this means that every shadow root needs to be <style>
#container {
--some-styles: some-values;
}
</style> Then I have unintentionally targeted any element that happens to use My inclination is that the best way for dealing with stylesheets not designed for shadow roots is just to do what @matthewp suggested and add the stylesheets that need to be inherited into each root. I think it would be good though if
This is another place where an API would be nice, e.g. give it an element and gets a list of rules that target that element e.g.: const el = document.createElement("div");
el.classList.add("my-class");
const rules = document.getMatchingRules({
element: el,
// Allow matching rules if it were in the DOM, without actually needing to add it to the DOM
where: {
anchorElement: document.body,
relativePosition: "child", // Or nextSibling, previousSibling etc
},
});
const sheets = rules.map(rule => rule.parentStyleSheet);
// Do whatever with sheets Like above, this would be more generally useful for building things like |
@rniwa any thoughts about this:
We pretty constantly get questions about how to use I feel like we really need some answer for those who want to incrementally adopt web components into existing apps with existing styling. It's a huge use case. |
Can't we already do that by inserting that the same stylesheet into the shadow root? |
Potentially... can we make style sheets created by |
Why would that matter? You can just insert a new style / link, right? |
I presume the pattern we're talking about is a component iterating over the stylesheets in it's root and copying them to into its shadow root. I think it'll be easiest if no matter where the stylesheets came from they could be added to the shadow root with one API. Also, if this is left to userland it'll be very difficult to do correctly. First it'll have to look for three different types of styles in its root: |
Could a key be added to this.attachShadow({
mode: 'open',
adoptHostStyles: true
}); I would imagine, then, that any local styles (say via a |
I really like the idea of an opt-in approach to allowing styles to cascade. Personally, the encapsulation is a big part of why we wanted to use web components for our design components but I can understand why that's not always the aim. I like the idea of having at least 2 options:
Having a separate property like Perhaps the default being something like: this.attachShadow({
mode: 'open',
styles: 'closed'
}); |
I'm wondering if an approach like I described above keeps us open to supporting CSS Modules should they ever be included in the spec and in the interm, allows us to pass in Stylesheet objects or StylesheetLists. |
If non-constructed stylesheet were adoptable, that would be a potential solution. |
+10000 for this idea for the ability to write form-associated custom elements that use native form inputs. |
@bahrus RE:
This is a hugely nice feature of Stencil that I find myself taking advantage of when building form-associated custom elements, but I have come across bugs with this, namely this one ionic-team/stencil#2801 which has to do with their internal logic that relocates slotted content in non-shadow components. |
Aurelia seems to be doing something similar. |
FWIW, we need to very carefully understand how a concrete proposal for this will work with declarative shadow DOM and scoped custom element registry. |
I don't have that concrete of a proposal in mind. I would love to figure out more constraints in the area before getting too specific. If any implementors have ideas how something that addresses the needs here could practically work that'd be great. I do think that there are likely straight forward answers for how something like this would interact with declarative shadow DOM and scoped custom element registries. For declarative shadow DOM, this would need to be a mode describes in attributes, so that the declarative shadow root is created in the right mode. And right now I don't see any interaction scoped custom element registries if this is completely on the shadow root side. A scoped registry shouldn't effect the mode of a shadow root. |
This still won't address the issues that @dflorey described in #864: an end user, importing and using 3rd party elements, can not force the components to call their own People are still going to be asking how to style 3rd-party elements from outside when the element authors have not called |
The should not be able to. A component should only expose internal DOM to styling if it opts-into it. Otherwise it's taking on potentially breakable API contracts it might not want to. If a user patches |
It seems to me that this is already somewhat solvable in userland using
I ran a quick benchmark and, in Chrome at least, there doesn't seem to be a big perf difference between injecting the same stylesheet into multiple shadow roots versus just having a single The main blocking issues with this approach seem to be:
Maybe solving the first issue ( |
Chrome's style system looks very different from Firefox's / Safari's, fwiw. Chrome collects invalidations globally, not per shadow root. So we discussed some of this today, and there are different use-cases. From what I understand, the most common one (from @justinfagnani / @bkardell) was ability to import "global" stylesheets into the shadow root automatically. A proposal could be something like: host.attachShadow({ importGlobalStyles: true }) // Or such Combined with something like In which order / with which priority / etc these sheets would apply seems a bit TBD and probably requires CSSWG discussion. It looks sorta like a "page UA sheet" of sorts, but I don't know if we want it to behave similarly to how UA sheets behave ( But then there was another use-case from @gregwhitworth which (if I understood correctly) would additionally allow descendant selectors on those stylesheets to pierce through, |
@emilio I spun up a document here so that @justinfagnani @bkardell @dfreedm and others can start putting pain points and the various gradient of web component capabilities that are currently bundled with encapsulation to have meaningful discussions around them each on their own merits. https://docs.google.com/document/d/1SToB0yip8tFvJSY9rFQDhUVTr8GUjdYFGFWhAq9NQds/edit I'm sure many can add concrete use-cases to each of them so please do so in a meaningful way (eg: please don't write numerous paragraphs for your usecase :) ) |
@emilio I don't think that the global scope should be specialized in any way - that would limit composability. I think it would be more ideal, and work similarly, is allowing shadow roots to be open to styles from their containing scope. Shadow roots would transitively be open to document styles if all ancestor scopes were open. So something like: host.attachShadow({ mode: 'open', openStyleable: true }); |
@rniwa Yeah, what Justin described. Framework components don't use ShadowDOM as core part of their implementation, so what I mean is that when you make a "composed tree" with frameworks (React/Vue/etc), that "composed tree" ends up as a light tree without ShadowDOM, and because of that document styles ("global styles" in typical framework terms) apply everywhere. I want to write custom elements with ShadowDOM and style scoping, but at the same time I want to simply stick Bootstrap (or alternatives) in my top level HTML file and have it simply work. This is easy with Framework components out of the box, but not yet with Custom Elements that have shadow roots.
Various proposals, ideas, and desires from the above conversation want this in different ways essentially, so it is indeed related. Allowing a selector to be cross-root (to behave like "global styles" in Frameworks(React/Svelte/etc) that have no ShadowDOM) is essentially the same behavior that styling the native composed tree (with enabling cross-root selectors, or something) would have. In various definitions of "open stylable" web components above, this end result is desired. Maybe the OP is a specific way to allow styles across roots, and perhaps what I Iightly expressed is a different approach (and very unspecified, not really a technical proposal, more of a description of an end result), but overall this thread has become also about various use cases not covered by the OP as well as alternative potential solutions to those use cases. |
I don't think we want to conflate those use cases with what this issue is tracking, which is about applying CSS rules that are defined in the document level. Whether people want shadow crossing combinator or not, or whether that topic is related to this issue or not, it should be tracked in a separate issue. It's not helpful to further expand the scope of the issue, which already has 250+ replies. |
As the person who said this, please let me reemphasize the word combinators and the previous elaboration that to me |
(Github Issues sadly don't have a reply-to-comment button, I am talking about your whole comment, in this thread (original comment, shadow layer proposal, layers access @scope, syntax) I share your optimism that combining without conflating is possible. The shadow layer proposal is in two parts: declarative shadow DOM, and syntax(s?) to access page styles (the ones that are already on the page outside the shadow tree, not ones that can be pulled in through a link or import tag which already work in shadow trees). Declarative shadow DOM solves an even more fundamental "mistake" than you reference. The declarative subtree problem in markup languages predates even HTML itself. Perhaps the old object tag debacle delayed an HTML solution by a decade, but the problem was so long and so well known when modern shadow DOM was introduced in 2011 that providing a Javascript-only shadow tree to me was just a huge mistake. But that is behind. Anyone using or authoring a web component (or designing a shadow tree) now can use declarative shadow DOM. So to me of course any proposal now would use it to "pull" in page styles. And any syntax would therefore of necessity be declarative and work from inside, not outside, the shadow tree. To write a POC, I had to pick only declarative syntaxes that are polyfillable and spec-defensible. I even explored how to synchronize multiple different syntaxes. I am a little surprised one of those syntaxes is so intuitive that it is even used to point out what you can't do with @layer today anywhere else on a page. So please, any syntax is fine by me, but I do think a polyfillable syntax would be much preferable overall. All syntaxes, even on a style tag, like all solutions will have tradeoffs, to me it is a question of picking the best tradeoffs overall. Yes, to me a solution would be similar to the way we can import entire CSS files into layers and into shadow trees now -- I was doing that into declarative shadow trees, but got annoyed reimporting resets.css files and relinking components.css files. And since to me @scope inside a shadow tree is a good thing, @scope inside a shadow tree could also fix shortcomings in ::slotted (the bottom side of encapsulation). |
Updating the shadow layers proposal, readme, and user story tests in response to: @knowler Allow layers to use different names in different contexts #10091 @mayank99 interweaving priorities of inner and outer context layers @mayank99 referencing and ordering of unlayered layers (?), see Allow authors to explicitly place unlayered styles in the cascade layer order #6323 @mirisuzanne "So we absolutely want ways to put sheets or scopes (or whatever) into layers." @mirisuzanne referencing named @sheet groups ("If we just want easy access to named chunks of CSS, we have @sheet") (see Multiple stylesheets per file #5629) The updated syntaxes below are polyfillable (and thus require no change to how @layer currently works), as demonstrated in the user story tests. Adding a layer renaming syntax:
//Inherit resets layer as higher priority renamed layer:
@layer inherit.resets.as.shadowresets, resets, shadowresets;
//Inherit resets layer as lower priority renamed layer:
@layer inherit.resets.as.shadowresets, shadowresets, resets;
//Interweave priorities of outer and inner context layers:
@layer inherit.A.as.outerA, inherit.B.as.outerB, outerA, A, B, outerB; The .as. items can appear in any order in the @layer statement rule, because the renamed layer name must also appear in the @layer statement rule. Inherit @scope page style The user story "Inherit @scope page style" that brings an outer context @scope rule into a shadow tree was already handled in the original shadow layers proposal, but now works with layer renaming. See user story test 17 "Inherit @scope page style". Adding a group referencing mechanism
//Inherit unlayered page styles as layer named unlayered:
@layer inherit.unlayered.as.unlayered, unlayered;
//Interweave priorities of outer layered and unlayered styles:
@layer inherit.layered.as.layered, inherit.unlayered.as.unlayered, layered, A, B, unlayered;
//Inherit all outer page styles:
@layer inherit.layered.as.layered, inherit.unlayered.as.unlayered, layered, unlayered;
At-rule support detection in @supports is not available, so @sheet would not be polyfillable (see Multiple stylesheets per file #5629), so the shadow layers POC does not implement @sheet. However, @layer is widely deployed so polyfillability is not needed for it, and @layer also provides the essential priority mechanism. Nonetheless, an @sheet supporting syntax would be possible: //Inherit named @sheet as layer
@layer inherit.sheet.mysheet.as.mysheet, mysheet; |
I don't know where that syntax even comes from, but it seems extremely out of place. Something like |
@DarkWiiPlayer I made a proposal for a standalone CSS feature which would use a syntax like that. I believe the dots syntax that @robglidden included above is specific to the shadow layers proof of concept. In general, I think it would be helpful to keep proof-of-concept specific syntax out of this thread as it’s too hard to keep track of all of the proposed syntaxes as well as the proof of concept syntaxes. |
Yes, that's the same issue I referenced above and previously. I think it is very insightful, thanks for proposing! |
The simpler forms ( But even a prototypical 27 Markdown component with automatic page styles These directly address the subject of this issue and work with a declarative shadow DOM, a shadow tree created with this.attachShadow(), cascade @layer, and @scope without breaking or requiring change to any of them. And it's polyfillable. |
I updated the shadow layers proposal to put both functionality and syntax in the broader context of:
See Add an adoptStyles capability in shadowRoots (WICG 909: open-stylable Shadow Roots) #10176. Table of Contents
|
I've been thinking a lot about open-styleable these past few weeks, and I've come up with a very simple-yet-powerful proposal that tries to avoid introducing too many new concepts. All feedback welcome! The road to open-styleableFirst, lets examine the currently available features that will play an important role in open styling. The most important one of them is There's an active/upcoming discussion around making It's worth mentioning some other features, but I will put them in disclosures, since this comment is already getting too long. declarative shadow DOMDSD greatly lowers the barrier to creating shadow-roots, but at the same time introduces important constraints. Declaratively created shadow-roots are often not tied to custom elements, and they cannot be observed on the client.
|
@mayank99 I really like the adoption aspect of this via // in an application:
import { designSystemConfig} from "some-design-system"
designSystemConfig.adoptPageStyles("sheet1 sheet2")
import { lotsof, components, here } from "some-design-system" and in a component: // within the design system
import { designSystemConfig} from "../config.js"
this.attachShadow({ mode: "open", adoptPageStyles: designSystemConfig.getPageStyles() }); Or of course in server-rendering there could be a mechanism to specify/inject the right values for DSD templates. |
@mayank99 i also agree 100% with the shape of this api. It’s minimal new concepts that dovetail nicely with existing concepts. i agree that named sheets are the right granularity for filtering rulesets before the cascade. It also solves @mirisuzanne concerns by not using fyi, here is @justinfagnani proposal for That proposal already accounts for named sheets. |
Isn't it already possible to share styles between the page and component? The user of your component can write this: import stylesheet from '/shared-styles.css' with {type: 'css'};
document.querySelector('whatever-element').shadowRoot.adoptedStyleSheets.push(stylesheet); And they can request the same stylesheet in the <link rel="stylesheet" href="shared-styles.css"> |
@o-t-w that's only true in very limited situations where the owner of the document knows what components to inject styles to, and the shadow roots are open, and the component doesn't somehow remove anything from adoptedStyleSheets. And it doesn't work for existing pages that don't have such a script. |
Is there any decent workaround currently to use a CSS framework with web components? |
@KurtCattiSchmidt, @dandclark, and Tien Mai have provided an excellent analysis/proposal loosely titled "Declarative shadow DOM style sharing". Framed as "a starting point for engaging the community and standards bodies in developing collaborative solutions fit for standardization", as I understand it, in my words it envisions:
Many thanks for potentially moving forward multiple issues. I have made some suggestions in this slide deck with examples on github. |
Note that declarative stylesheet sharing is for a completely different use case than open styleable.
|
We keep hearing that the strong style encapsulation is a major hinderance - perhaps the primary obstacle - to using shadow DOM.
The problems faced are usually a variant of trying to use a web component in a legacy context where global styles of the page are expected to be applied deeply through out the DOM tree, or a modern context with tools that don't work with scoping, eg Tailwind.
I've see requests to workaround encapsulation formed in a number of ways:
<slot>
working (which obviously isn't).Would it be possible to address some of these difficulties with scoping more or less directly, by adding a new shadow root mode that allows selectors to reach into a shadow root from above? Something like:
Application developers could then make sure that elements from the root of the page on down to where they need styles to apply use open-stylable shadow roots. Library authors could offer control over the mode for legacy situations, etc.
I'm not sure how combinators would work here. ie, would
.a > .b {}
apply to<div class="a"><x-foo>#shadow<div class="b">
? It may be that's not even needed to be able to use most of the stylesheets in question, which often rely more heavily on classes than child/descendent selectors.Would this be viable performance-wise?
Related to #864
The text was updated successfully, but these errors were encountered: