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

"open-stylable" Shadow Roots #909

Open
justinfagnani opened this issue Dec 10, 2020 · 269 comments
Open

"open-stylable" Shadow Roots #909

justinfagnani opened this issue Dec 10, 2020 · 269 comments

Comments

@justinfagnani
Copy link
Contributor

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:

  1. Turn off shadow DOM (as is an option in LitElement), but keep <slot> working (which obviously isn't).
  2. Allow styles to apply from the outside
  3. Add a way to automatically inject page styles into shadow roots.
  4. Implement a custom CSS scoping mechanism for when shadow dom is off
  5. Some specific library, eg Tailwind, converted to work with shadow roots
  6. etc...

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:

this.attachShadow({mode: 'open-stylable'});

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

@rniwa
Copy link
Collaborator

rniwa commented Dec 10, 2020

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.

@bahrus
Copy link

bahrus commented Dec 11, 2020

I think there's something to be said for this idea:

Turn off shadow DOM (as is an option in LitElement), but keep <slot> working (which obviously isn't).

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.

@LarsDenBakker
Copy link

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.

@justinfagnani
Copy link
Contributor Author

@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 <style> and <link> elements, and since their stylesheets are not adoptable, you couldn't get changes to them. You'd also need to patch adoptedStyleSheets.

@matthewp
Copy link

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

@justinfagnani
Copy link
Contributor Author

@bahrus I don't think <slot> makes much sense outside of shadow DOM because you end up with contention of the children. Composition works because the light and shadow DOM are separated.

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 <slot> because that would be projecting content from an outer scope. So i'll invent <content> and <content-slot> to allow projection between siblings:

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

@justinfagnani
Copy link
Contributor Author

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

@Jamesernator
Copy link

Jamesernator commented Dec 14, 2020

I am wary of the all-or-nothingness of open-stylable in two ways as it basically makes it impossible to actually isolate some styles.

e.g. Suppose you want to use tailwind at the root level, this means that every shadow root needs to be open-stylable. But now what do I do if I want to isolate styles to a shadow root? If I add the following sheet to my shadow root:

<style>
    #container {
        --some-styles: some-values;
    }
</style>

Then I have unintentionally targeted any element that happens to use id="container" within nested components.


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 StyleSheet and StyleSheetList had change events so that we could respond to changes more accurately. (This would be useful beyond this use case as well, e.g. for building things like StyleObserver, etc).

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 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 StyleObserver, css polyfills/extensions/etc.

@justinfagnani
Copy link
Contributor Author

@rniwa any thoughts about this:

If selectors weren't evaluated across shadow boundaries, would it be possible to run them whole inside a child shadow root?

We pretty constantly get questions about how to use <slot> without shadow DOM. Being close to the browser/spec, we may know that question doesn't make a lot of sense, but the intention is pretty clear: users want the composition of shadow DOM without the upper-bound style scope.

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.

@rniwa
Copy link
Collaborator

rniwa commented Feb 27, 2021

@rniwa any thoughts about this:

If selectors weren't evaluated across shadow boundaries, would it be possible to run them whole inside a child shadow root?

Can't we already do that by inserting that the same stylesheet into the shadow root?

@justinfagnani
Copy link
Contributor Author

Can't we already do that by inserting that the same stylesheet into the shadow root?

Potentially... can we make style sheets created by <style> and <link> adoptable?

@rniwa
Copy link
Collaborator

rniwa commented Feb 27, 2021

Can't we already do that by inserting that the same stylesheet into the shadow root?

Potentially... can we make style sheets created by <style> and <link> adoptable?

Why would that matter? You can just insert a new style / link, right?

@justinfagnani
Copy link
Contributor Author

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: <style>, <link> and adopted styles. Then it'll have to handle loading state the tags. Finally it'll have to handle dynamic updates with a mutation observer for tags, and I'm not sure what for adopted stylesheets.

@calebdwilliams
Copy link

calebdwilliams commented Feb 27, 2021

Could a key be added to ShadowRootInit that would cause the shadowRoot.styleSheets or shadowRoot.adoptedStyleSheets to mirror the contents of document.styleSheets shadowHost.styleSheets?

this.attachShadow({
  mode: 'open',
  adoptHostStyles: true
});

I would imagine, then, that any local styles (say via a <style> block) would append to the end of the StyleSheetList, but keeping those lists in sync could be gnarly from an implementation perspective.

@castastrophe
Copy link

castastrophe commented Mar 5, 2021

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:

  1. Allow all CSS to penetrate the shadow DOM using an opt-in approach such as
    this.attachShadow({
      mode: 'open',
      styles: 'all'
    });
  2. Allow only specific stylesheets to penetrate the shadow DOM:
    this.attachShadow({
      mode: 'open',
      styles: [stylesheetPointer] // array of pointers to specific stylesheets?
    });

Having a separate property like styles that can accept a few different inputs gives us a lot of flexibility to add functionality as we go.

Perhaps the default being something like:

this.attachShadow({
  mode: 'open',
  styles: 'closed'
});

@castastrophe
Copy link

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.

@calebdwilliams
Copy link

If non-constructed stylesheet were adoptable, that would be a potential solution.

@eriklharper
Copy link

eriklharper commented Mar 10, 2021

Turn off shadow DOM (as is an option in LitElement), but keep slots working (which obviously isn't).

+10000 for this idea for the ability to write form-associated custom elements that use native form inputs.

@eriklharper
Copy link

@bahrus RE:

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.

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.

@bahrus
Copy link

bahrus commented Mar 10, 2021

Aurelia seems to be doing something similar.

@rniwa
Copy link
Collaborator

rniwa commented Mar 12, 2021

FWIW, we need to very carefully understand how a concrete proposal for this will work with declarative shadow DOM and scoped custom element registry.

@justinfagnani
Copy link
Contributor Author

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.

@trusktr
Copy link

trusktr commented Apr 3, 2021

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 attachShadow methods with a setting to make styles leak in, except by monkey patching attachShadow (which may not be possible with declarative ShadowDOM and elementInternals where attachShadow is not called unless we specify in the spec that attachShadow always will be called).

People are still going to be asking how to style 3rd-party elements from outside when the element authors have not called attachShadow with open-stylable or similar.

@justinfagnani
Copy link
Contributor Author

an end user, importing and using 3rd party elements, can not force the components to call their own attachShadow methods with a setting to make styles leak in

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 attachShadow() or the element definition directly, they know they're off the well-lit path for that element.

@nolanlawson
Copy link

"how do I use bootstrap in web components"

It seems to me that this is already somewhat solvable in userland using adoptedStyleSheets. As long as we're talking about open shadow roots, a page author can traverse the DOM, find all shadowRoots, and append a constructable stylesheet into their adoptedStyleSheets.

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?

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 <style> tag in the <head> (using light DOM). So it seems viable perf-wise.

The main blocking issues with this approach seem to be:

  1. adoptedStyleSheets only work with constructable stylesheets. So e.g. a remote bootstrap.css wouldn't work unless its content could be injected into a constructable stylesheet (and dealing with relative paths, @imports, etc.).
  2. As previously said, you would have to use MutationObserver or something to watch for any DOM changes (unless you have full control over the page).
  3. It doesn't easily solve scoping, e.g. having one part of the DOM use Bootstrap whereas another uses Bulma. But I'm not sure how common of a use case this is.

Maybe solving the first issue (adoptedStyleSheets requiring constructable stylesheets) would solve most usecases of "I just want to use Bootstrap everywhere"?

@emilio
Copy link

emilio commented Apr 22, 2021

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 <style> tag in the (using light DOM). So it seems viable perf-wise.

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 <style global> or <link rel=stylesheet global> or whatever.

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 (!important in UA sheets can't be overridden by the page for example, in this case it wouldn't be overridable by the web component author for example), or maybe it'd be easier to just treat them as "earlier in source order" or such.

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, querySelectorAll to break the shadow DOM boundary, etc. I think that'd be much more challenging to implement.

@gregwhitworth
Copy link

@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 :) )

@justinfagnani
Copy link
Contributor Author

@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 });

@robglidden
Copy link

To me, the key open question that needs to be answered is whether selective adoption of document-level stylesheets is necessary or not. That is, should a component or users of the component need to be able to filter (i.e. selectively apply) which stylesheet or style rules apply to its shadow tree.

I think filtering is needed.

There are numerous use cases and comments in this and the preceding related issue saying or assuming as much.

More fundamentally:

Use cases are intertwined.

Whether simple (I just want my page resets to work in my shadow tree) or most painful (component and context are controlled by different entities) or in between, use cases trace to the same shadow encapsulation, and loosening encapsulation will likely impact many if not all in some way.

Declarative shadow DOM is required.

To me, declarative shadow DOM is required from the onset not just because it is a new thing so logically it must be supported too, but because shadow trees are just HTML and they are what are doing the encapsulating to loosen. And they are a part of the solution.

So Javascript-only solutions are basically out. And polyfillability though not a strict requirement is in this case something of an acid test for completeness.

But also declarative shadow trees already by design address opt-in for imperatively-created web components.

Context-aware opt-in protection is needed.

I don't think an opt-in flag or mode that just brings in all page styles would work as expected in all cases. Some shadow styles could unexpectedly lose. Or get pulled back into a specificity race that could feel like shadow trees just gave up.

Prioritization is required.

Or maybe just unavoidable, since unlayered styles are of necessity assigned a layer priority. And a limited form of priority for shadow trees already exists in the context step, so any brought-in styles would pass through that step too.

The shadow layers proposal addresses these requirements, and other proposals and syntaxes certainly could too.

I hope decisions like filtering-yes-or-no will help move this issue forward to actionable proposals and resolution. We have new tools to do that.

@DarkWiiPlayer
Copy link

@matthewp

An attribute on the <template>, to me doesn't qualify as "page doesn't have to opt-in". They do have to opt-in, either by adding that attribute themselves or using a library or some other tooling to do it for them. Maybe you don't consider that to be the page opting in. And that's ok! I'm curious if others who value encapsulation feel the same way.

If adding an attribute doesn't count as "page opting in" then why do the other solutions? A library or tool can add a style tag with some CSS too. I think all of the proposed solutions fit the requirement if libraries are allowed.

I think it's important to consider where the attribute is and who is in charge of setting it. The <template> element would normally be rendered by the component itself, via some sort of server-side rendering mechanism. This is different than the global CSS having to consider the component.

The point here is for a component author to provide a component that, without any further changes from the user, can be dropped into a website and adopt the website's styling. So an opt-in from within the component is perfectly in line with the goals of this proposal, while an opt-in by the website author is not.

The <template> is really just a special case of attachShadow, where the component provides some server-side code to pre-render the contents of its shadow-DOM, but in these cases the attribute on the <template> is still under the control of the component author.


Some more thoughts on this topic as an occasional component-author:

Pulling in all styles as a default seems essential to this feature. For a component that really "just works" there can be no requirements to separate styles out on the website into what should be used by a component and what shouldn't.

Likewise, having the option of writing my components to be configurable, meaning users could instruct my component on which of their styles should be used (for example via layers) would also be a very valuable feature to me.

One question that presents itself after considering that previous thought is that changing which styles get used in the shadow dom might be essential in avoiding life-cycle headaches. I think it is a reasonable expectation for a component to have figured out what styles to pull by the time it attaches its shadow-dom, but my ideal API for a component would allow for something like this:

import "CoolComponent.js"

component = document.createElement("cool-component")
component.styleScope = "base" // I, the website author, put my global styles in this layer

someOuterElement.append(component)

What this means is that, if CoolComponent wants to attach its shadow-dom in the constructor, it won't yet know that the user wants to only pull in the "base" layer, so it would have to be able to change this later on.

As I mentioned, this isn't strictly necessary, as the component could attach its shadow dom in the connectedCallback, and the user would have to take care to set the attribute before the first time the component gets inserted into the document, but that's just clutter to the mental model of what happens, and yet another foot-note in the documentation making the component more unwieldy.

I still think a dedicated element inside the shadow tree would be the best interface from a user's perspective, as that would easily allow making this change even after the shadow-dom has been attached to the component, whereas a new mode value would make it very difficult to design an API that isn't confusing.


@mirisuzanne I don't think layers would be a bad mechanism to select styles to be pulled into a shadow-dom. Conceptually, they seem like the right way of handling this. Whether or not this will incentivise users to start using layers as a more granular API to style components seems like a different question that should better be addressed by providing a separate way of doing these more granular styling tasks.

Ideally, layers would be a way of broadly narrowing down what styles to pull into a component, and some other mechanism (I like the idea of extending @scope for this) would allow website authors to be more granular about styling the inside of a component.

Put differently: This issue is about components pulling styles in, with layers only serving as a way of optionally narrowing down the affected styles; there is a separate need for a mechanism to push styles into a component that this feature could be misused for.

Am I understanding the criticism of the layers-based approach correctly, or am I missing the point?

@matthewp
Copy link

matthewp commented Mar 16, 2024

Note that GitHub's quoting mechanism seems to be broken at the moment so I'm omitting some quotes here unfortunately

@DarkWiiPlayer

I'm fine with this the definition of "opt-in", so going back to @justinfagnani's comment:

I thought an attribute might be tenable here for the layer approach, but the original idea is to not require any opt-in from the page.

What proposals in this thread require an opt-in from the page, if we agree to @DarkWiiPlayer's definition of opt-in? A server-side component framework can add CSS to a page just as easily as it can add an attribute to an element. Am I missing something? All of these proposals seem equally viable as far as this requirement is concerned, to me.

@DarkWiiPlayer
Copy link

A server-side component framework can add CSS to a page just as easily as it can add an attribute to an element.

I think you're conflating two ideas here:

In cases where the entire content generation of a site is handled by a single framework, then that framework could easily insert code at several points, meaning it could create a custom element and add corresponding CSS elsewhere to push styles into it.

This much is correct. However, you're reducing the entire feature to this specific case. The point of web component is precisely to not depend on this type of architecture where a single framework controls the entire stack, but where component authors can independently write framework-agnostic components that any website can just use.

In the most basic case, a website is written in plain HTML and CSS, and the author simply loads a JS module from jsdelivr and starts adding <custom-element>s to their document, and expect the <button>s inside the shadow-dom to look the way they do in the rest of the site.

Please keep in mind that the web spans a massive range of scales and architectures, from massive companies like google who have the resources to just built their own entire framework to handle things coherently, to the gym around the corner giving 50€ to the owner's 16yo nephew to build them a website. Web components should aim to work for all of them :)

@robglidden
Copy link

@DarkWiiPlayer, @matthewp:

I hope it is clear that the shadow layers proposal is not proposing to put an attribute ala shadowlayers="inherit.reset" on a template tag.

Whether the template is part of an attachShadow() javascript web component initialization approach or more importantly is part of a declarative shadow DOM (it is not polyfillable among other reasons).

The proposal treats both an attribute wherever placed and a potential attachShadow() option as just a convenience (and speculative coordination enhancement), not a requirement.

As to syntaxes, my preference is neither an attribute nor an attachShdow() option and just put directly inside a shadow tree:

@layer inherit.reset;

To me that is intuitive, supports declarative shadow trees, provides opt-in protection for web component authors who need it, avoids the need for a new mode or attribute, and avoids FOUC.

@DarkWiiPlayer
Copy link

@robglidden

To me that is intuitive

Any strong feelings on CSS @-rule vs. a new tag? To me it just seems slightly more convenient and semantically appropriate to put this in the HTML, but I don't feel too strongly about this.

@matthewp
Copy link

@DarkWiiPlayer

I think you're conflating two ideas here:

In cases where the entire content generation of a site is handled by a single framework, then that framework could easily insert code at several points, meaning it could create a custom element and add corresponding CSS elsewhere to push styles into it.

This much is correct. However, you're reducing the entire feature to this specific case. The point of web component is precisely to not depend on this type of architecture where a single framework controls the entire stack, but where component authors can independently write framework-agnostic components that any website can just use.

It doesn't need to control the entire stack. The <style> tag can be output inline right next to the shadow template:

<style>@layer or @scope or whatever ...</style>
<my-element>
  <template shadowrootmode="open">...</template>
</my-element>

@robglidden
Copy link

@DarkWiiPlayer:

@layer works inside a shadow tree now:

<my-element>
    <template shadowrootmode="open">
        <style>
            @layer reset, BETTER-BUTTONS;

            @layer BETTER-BUTTONS {
                button {
                    border: thick dashed red;
                }
            }

            @layer reset {
                button {
                    border: thin solid black;
                }
            }
        </style>
        <button>Dashed red button</button>
    </template>
</my-element>

It just can't reference a layer from the outer context (like inherit.BETTER-BUTTONS).

I am not sure what you mean by a new tag.

@DarkWiiPlayer
Copy link

@DarkWiiPlayer

I think you're conflating two ideas here:

In cases where the entire content generation of a site is handled by a single framework, then that framework could easily insert code at several points, meaning it could create a custom element and add corresponding CSS elsewhere to push styles into it.

This much is correct. However, you're reducing the entire feature to this specific case. The point of web component is precisely to not depend on this type of architecture where a single framework controls the entire stack, but where component authors can independently write framework-agnostic components that any website can just use.

It doesn't need to control the entire stack. The <style> tag can be output inline right next to the shadow template:

<style>@layer or @scope or whatever ...</style>
<my-element>
  <template shadowrootmode="open">...</template>
</my-element>

...and how would that help, exactly? That way you could define new styles to push inside your custom element, gaining nothing over just putting the styles inside the actual shadow DOM.

@mayank99
Copy link

Did we ever compile a list of user stories? I shared a few above. I believe @Westbrook was trying to collect some more, but I can't find it.

I think when discussing solutions, it would be useful to see which user stories are/aren't solved and how.

@robglidden
Copy link

@mayank99

Yes, a compiled list of user stories and requirements would be helpful.

These are the use cases addressed by the shadow layers proposal. As mentioned, I think they may be a bit over-broad particularly as to coordination scenarios; I'd suggest essential requirements.

The user stories you listed are ones I have encountered also, and hope they are covered by the shadow layers proposal and any other proposal.

@DarkWiiPlayer
Copy link

This is close to just being a variation of the second user story from that comment, but applied to my everyday work experience:

When developing web applications for internal use, I want to re-use web components that represent common logic across various projects regardless of what tech stack they individually use. When frameworks are used that require extra classes, I want to extend the components to use templates and have the framework classes available automatically inside the component.

It might be worth pointing out that mixins will eventually make their way into CSS, so depending on what the specifics end up being, the case where a framework needs classes might be alleviated by the ability to apply these classes to plain elements as mixins. Although I didn't see too much enthusiasm for the idea of allowing classes as mixins directly.

@mirisuzanne
Copy link

mirisuzanne commented Mar 21, 2024

To me, "access" couldn't happen at either the layers or scope proximity step in the cascade. It must happen before, I assume in the context step, and would pass first through the element-attached styles (style attribute in cascade 6) step.

To clarify: 'Cascading' happens as a resolution step after 'filtering' is complete. That's my point, they are separate steps. While both layers and scope show up in the 'cascading' process, with an impact on declaration priority:

  • The @layer syntax is only a mechanism for managing cascade priority. That's all it does currently.
  • The @scope syntax is a selector mechanism primarily for managing access. It's primary function is 'filtering' before the cascade. But (like other selectors) it has some heuristic implications on cascade priority.

One of them is a filtering mechanism (with some cascade implications), and the other one is a cascading mechanism (full stop). That was very intentional. Layers were designed specifically to provide a cascade mechanic that would not be entangled with filtering. The fact that @scope shows up in the cascade is the most controversial thing about it. Linking those two things is risky, as we've seen over the years managing selector specificity.

When it comes to filtering which styles are attached, there are two existing mechanisms that are more appropriate:

  • If we're defining styles that apply to a specific context, that's @scope
  • If we just want easy access to named chunks of CSS, we have @sheet

In both cases, layers have roughly the right shape for what we want - letting us divide and label parts of a CSS file. So that makes it very tempting to use @layer syntax for the whole thing. But @layer is the only unencumbered cascade mechanic we have, designed specifically to disentangle filtering from cascading – because otherwise it was impossible to control cascade priority without also manipulating selection. Let's not make that mistake again.

Still, I agree that layers do seem essential to the solution here. We're concerned with both which styles apply, and also how they cascade. So we absolutely want ways to put sheets or scopes (or whatever) into layers. That would be similar to the way we can import entire CSS files into layers. And it would be useful for component authors to expose layers, for more nuanced cascading between page and shadow-dom.

We should be able to combine these features, but we should not conflate these features.

@DarkWiiPlayer
Copy link

@mirisuzanne Thank you for that explanation. Looking at it that way, it makes sense how @layer might be a bad choice for narrowing down which styles to pull into a shadow-dom.

My personal mental model of "layers" is that in practice they end up representing something like "phases" of styling: first we reset browser defaults, and get to some neutral state; then we apply a framework, and get to a basic styled state; then we apply site-wide styles, and get to an overall finished page; then we might add some more specific layers based on what page we're on, etc.

In that sense, it makes sense to express a concept like "Take the resets and the framework styles from the light dom, but not the other stuff" using @layer names.

@sheet seems like a good alternative for this, at least on a conceptual level, although at a glance it seems like it could end up lacking some relevant features. The first thing that comes to mind, without having read the whole proposal yet: It would be useful, if not essential, to name a sheet when <link>ing it directly from the HTML, otherwise we might end up with situations like "I want to tell this component to only pull resets, but my resets.css is just linked in the HTML, so now I have to change my HTML and wrap it in a <style> block, or worse, edit the resets.css itself and wrap the entire content in a @sheet rule", which I would find very undesireable both as a user and author of components.

As a more general design consideration for this feature, one could phrase it like this:

Using a web component should not dictate or narrow down how page authors can organise their CSS


Also, please note that this applies to the selection of which styles to inherit; it has nothing to do with the requirement to wrap inherited styles in a layer inside the shadow-dom, as that really is just a matter of handling cascade specificity (in the most common case, making sure no inherited styles end up overriding inner styles of the shadow-dom)

@mayank99
Copy link

mayank99 commented Mar 21, 2024

I completely agree that @layer is the wrong tool for selecting which styles make into shadow DOM. It just seemed convenient because it is an already-available way of grouping styles. I've been guilty of suggesting layers myself.

Also there is a big problem with the shadowlayers proposal (and some existing demonstrations like half-light): all document styles are wrapped in a layer. This may be fine for specific cases, but it's not a good default. In fact the default should be flipped, because that's how the cascade already works today: page styles cascade after the shadow-root's own styles. Perhaps more importantly, it should be possible to place different layers from the document into different spots (not possible if everything is wrapped in a single inherit layer).

A few comments up, I showed an example similar to this:

@layer inherit.A, A, B, inherit.B;

This would not work. Layers cannot be split like this. The browser will "autocorrect" it to:

@layer inherit.A, inherit.B, A, B;

I've been a huge fan of layers since day one, and I still think they'll help us manage the cascade priorities of "outside" vs "inside" styles. But this should be opt-in.

The solution to open-styleable should allow some way of using already-written styles.

  1. The vast majority of CSS written today (both outside and inside shadow-roots) doesn't use layers. These styles might already manage the specificity in a way that open-styleable will work fine out-of-the-box. For example, all styles in their reset might be deprioritized using :where and their utilities might be reinforced with !important.
  2. Similarly, a lot of CSS written today is already properly scoped (e.g. using build tools or namespacing). Open-styleable will work fine with these styles out-of-the-box.
    • In fact, there's good precedent for this: framework components (built with light DOM) distributed to npm are widely used (way more than shadow DOM is used). And it has been totally fine.
    • The key difference here is that the expectations for light DOM components were clear from the get-go, so they likely do not use vague classes like "button". Existing shadow DOM components never had this same expectation. That's why open-styleable shadow-roots should be opt-in, using a new attribute/property.

@DarkWiiPlayer
Copy link

In my opinion, although it isn't a very strong one, I think imported styles should not be added to a layer by default, but users should have the option to do so on demand.

I suspect the most common case will be that component authors will want their own styles to override the inherited ones and might therefore inherit them into a layer. If selectively inheriting different stylesheets (or layers, for that matter) then there should also be an option to inherit them into different layers, so, for example, resets could be overridden by internal styles, but some user-overrides would in turn override the internal styles if the author wants to give the user this much freedom.

@trusktr
Copy link

trusktr commented Mar 21, 2024

I am not entirely sure why shadow crossing combinators have such a mindshare in this issue and keep coming up.

The answer is simple: framework components f.e. in React, Preact, Svelte, Vue, Solid, Angular, etc, all have this feature out of the box. We need to make the migration story from framework components to custom elements easy for all web developers. Cross-root selectors like .foo .bar are essentially required for this.

Web Components are used to facilitate interoperability and bridge the gaps between the different tech stacks. In these use cases, "fitting-in" with existing CSS is highly desirable.

Cross-root selectors are required for this too. Currently custom elements with ShadowDOM get in the way of this, and many of the above proposed solutions are too complicated compared to framework components and global styles to be desirable.

I’ve said it before, but I’ll say it here for the record: making web component styling exactly like framework components would be a huge regression.

The idea isn't to make all web components stylable like light DOM, but to allow the web components author to choose to make their shadow DOM-using components stylable like light DOM. The alternative is to not use shadow DOM and lose composition.

I don't think it would be a regression, but perhaps page authors should be able to opt into global styling, including selectors like .foo .bar applying across roots, and the page author is then responsible for any ensuing style conflicts.

People haven't really mentioned it, but really what cross-root selectors means is styling the composed tree. It is simple and reasonable to want that.


All frameworks today (React/Vue/Svelte/Solid/etc) have both global stylability out of the box with cross-component selectors, and libraries or build features/plugins that enabled component-scoped styles.

I believe we should highly consider simple solutions that make both patterns possible, and right now we already have component-scoped styles, so we really just need a simple opt-in document-level global styling solution. Maybe also component-on-down scoping (like a component's style is global for all elements below it in the composed tree).

Other proposals in this thread may still be useful to add for better patterns (layers, scope, etc), but they are simply too complicated for what most people need coming from frameworks.

@rniwa
Copy link
Collaborator

rniwa commented Mar 21, 2024

I am not entirely sure why shadow crossing combinators have such a mindshare in this issue and keep coming up.

The answer is simple: framework components f.e. in React, Preact, Svelte, Vue, Solid, Angular, etc, all have this feature out of the box. We need to make the migration story from framework components to custom elements easy for all web developers. Cross-root selectors like .foo .bar are essentially required for this.

What do you mean by "all have this feature out of the box"? Clearly, cross-shadow combinator isn't a thing today.

People haven't really mentioned it, but really what cross-root selectors means is styling the composed tree. It is simple and reasonable to want that.

That sounds like an entirely different problem statement. Please go file a separate issue instead of conflating that in this issue.

@justinfagnani
Copy link
Contributor Author

justinfagnani commented Mar 21, 2024

I am not entirely sure why shadow crossing combinators have such a mindshare in this issue and keep coming up.

The answer is simple: framework components f.e. in React, Preact, Svelte, Vue, Solid, Angular, etc, all have this feature out of the box. We need to make the migration story from framework components to custom elements easy for all web developers. Cross-root selectors like .foo .bar are essentially required for this.

What do you mean by "all have this feature out of the box"? Clearly, cross-shadow combinator isn't a thing today.

I think he means that by using a proprietary composition mechanism and not using shadow DOM, the frameworks inherently let you shadow their composed tree, because the composed tree is the light DOM. So what would be to us cross-shadow combinators are to them just combinators that cross scope boundaries that the browser isn't aware of.

People haven't really mentioned it, but really what cross-root selectors means is styling the composed tree. It is simple and reasonable to want that.

That sounds like an entirely different problem statement. Please go file a separate issue instead of conflating that in this issue.

It's not unrelated though. I though such a thing was impossible, but if it were possible, I think it could subsume this proposal and probably be strictly better.

@robglidden
Copy link

@layer inherit.A, A, B, inherit.B;

There is a relevant open issue, Allow layers to use different names in different contexts #10091

Also, Collection of user stories #1052 is offering to collect user stories.

@robglidden
Copy link

Also there is a big problem with the shadowlayers proposal (and some existing demonstrations like half-light): all document styles are wrapped in a layer. This may be fine for specific cases, but it's not a good default. In fact the default should be flipped, because that's how the cascade already works today: page styles cascade after the shadow-root's own styles.

Cascade layers inside a shadow tree cascade independently of page styles (except the context step), but if this means that in a shadow tree unlayered styles should have a way to be at a user settable priority, there is an open issue Allow authors to explicitly place unlayered styles in the cascade layer order #6323.

@robglidden
Copy link

I am not entirely sure why shadow crossing combinators have such a mindshare in this issue and keep coming up.

The answer is simple: framework components f.e. in React, Preact, Svelte, Vue, Solid, Angular, etc, all have this feature out of the box. We need to make the migration story from framework components to custom elements easy for all web developers. Cross-root selectors like .foo .bar are essentially required for this.

There is also the :host-context() pseudo-class function which tests whether there is an ancestor, outside the shadow tree, which matches a particular selector. But it is not supported in either Safari or Firefox. And unlikely to ever be? In 2016, Apple stated its opposition to :host-context() as an anti-pattern that sets behavior of elements based on context, and subsequent discussion tends towards favoring dropping it from the specification.

@trusktr
Copy link

trusktr commented Mar 21, 2024

What do you mean by "all have this feature out of the box"? Clearly, cross-shadow combinator isn't a thing today.

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

That sounds like an entirely different problem statement. Please go file a separate issue instead of conflating that in this issue.

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.

@rniwa
Copy link
Collaborator

rniwa commented Mar 21, 2024

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.

@robglidden
Copy link

I am not entirely sure why shadow crossing combinators have such a mindshare in this issue and keep coming up.

As the person who said this, please let me reemphasize the word combinators and the previous elaboration that to me .foo .bar inside a shadow tree does matter.

@robglidden
Copy link

@mirisuzanne:

Still, I agree that layers do seem essential to the solution here. We're concerned with both which styles apply, and also how they cascade. So we absolutely want ways to put sheets or scopes (or whatever) into layers. That would be similar to the way we can import entire CSS files into layers. And it would be useful for component authors to expose layers, for more nuanced cascading between page and shadow-dom.

We should be able to combine these features, but we should not conflate these features.

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

@robglidden
Copy link

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.reset.as.shadowreset: uses an as keyword to assign a different layer name to an outer context layer when inherited into a shadow tree's inner context.
//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: inherits outer context unlayerd page styles
  • inherit.layered: inherits outer context layered page styles
//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;

@sheet

  • inherit.sheet.sheetname: inherits outer context @sheet as an inner context layer

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;

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

No branches or pull requests