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

Declarative CSS Modules and Declarative Shadow DOM adoptedstylesheets attribute #10673

Open
KurtCattiSchmidt opened this issue Oct 3, 2024 · 26 comments
Labels
addition/proposal New features or enhancements needs implementer interest Moving the issue forward requires implementers to express interest stage: 1 Incubation topic: script topic: shadow Relates to shadow trees (as defined in DOM)

Comments

@KurtCattiSchmidt
Copy link

What problem are you trying to solve?

Developers using Declarative Shadow DOM (DSD) do not have a way to share declarative stylesheets without using script. There are many workarounds, but none are ideal.

What solutions exist today?

If a developer wants to share styles between the light DOM and DSD, they can either:

a. Duplicate individual inline style rules between shadow roots. This is problematic because it often leads to lots of duplicated style definitions.
b. Use <link rel> tags for shared CSS files in each shadow root. This is problematic because external stylesheets are an asynchronous render-blocking resource, and could cause an FOUC.
c. Use the Javascript adoptedStyleSheets property on the declarative shadow root to share stylesheets. This is problematic because it can cause an FOUC and only works with Javascript enabled.

None of these options are ideal.

How would you solve it?

Declarative CSS Modules allow developers to define style sheets that by default do not apply to the main document, but instead are stored in the global Module Map. An adoptedstylesheets attribute on the <template> element will allow developers to opt Declarative Shadow DOM elements into sharing these stylesheets via module syntax.

Proposed syntax:

<script type="css-module" specifier="/foo.css">
  #content { 
    color: red; 
  }
</script>
<my-element> 
   <template shadowrootmode="open" adoptedstylesheets="/foo.css"> 
     <!-- ... -->
   </template>
</my-element>

The <script> tag allows for styles to be defined without impacting the main document. The adoptedstylesheets attribute on the <template> element will look up the module specifier and associate it with the referenced <style> block.

Explainer: https://github.com/MicrosoftEdge/MSEdgeExplainers/blob/main/ShadowDOM/explainer.md

Anything else?

No response

@KurtCattiSchmidt KurtCattiSchmidt added addition/proposal New features or enhancements needs implementer interest Moving the issue forward requires implementers to express interest labels Oct 3, 2024
@domenic
Copy link
Member

domenic commented Oct 3, 2024

The most alarming part of this proposal to me is introducing the specifier="" attribute. That seems like it would have significant implications for the module system.

How necessary is that to this idea? Could you instead use, e.g., the id="" attribute?

@rniwa
Copy link

rniwa commented Oct 3, 2024

The most alarming part of this proposal to me is introducing the specifier="" attribute. That seems like it would have significant implications for the module system.

That's the most noble part of this proposal. It's a feature which allows CSS module to be defined inline.

How necessary is that to this idea? Could you instead use, e.g., the id="" attribute?

I don't think we want to use id for this purpose since id's are not shared across shadow boundaries.

@annevk annevk added topic: shadow Relates to shadow trees (as defined in DOM) topic: script labels Oct 4, 2024
@dandclark dandclark added the agenda+ To be discussed at a triage meeting label Oct 16, 2024
@cwilso cwilso added stage: 1 Incubation and removed agenda+ To be discussed at a triage meeting labels Oct 17, 2024
@domenic
Copy link
Member

domenic commented Oct 19, 2024

I saw this was discussed at WHATNOT and some new issues were opened. Let it be clear that I don't consider my above objections resolved and I would not be OK with this proceeding further in its current state. This completely new way of manipulating the module map needs significant further discussion and ideally an alternative should be found that either generalizes beyond this specific new feature, or avoids manipulating the module map entirely.

If you want to open a new issue to start that discussion that might be good.

I don't think we want to use id for this purpose since id's are not shared across shadow boundaries.

This doesn't seem like a blocker to me.

Ultimately you have to decide whether you want to pierce shadow boundaries by using a global shared namespace, like the module map / global ID map, or whether you want to use a per-shadow root namespace. The current proposal wants a global shared namespace. So assuming that, some potential workarounds are:

  • Have the web developer hoist the shared stylesheets out into the global space, to make it more obvious that they're doing something that is not constrained by shadow DOM instead of having this implicit shadow-crossing behavior of populating the module map.
  • Have some new syntax, e.g. globalid="", for populating the global ID map while inside the shadow tree. This seems like a bad idea to me, but so does populating the global module map, and I'd rather we have DOM elements populate the global ID map than the global module map, since they're DOM elements.
  • Use some of the more esoteric techniques that people use to forward IDs across shadow boundaries, e.g. for ::part(), or the recent ARIA proposals.

@KurtCattiSchmidt
Copy link
Author

KurtCattiSchmidt commented Oct 21, 2024

If you want to open a new issue to start that discussion that might be good.

The module map behavior is probably the largest piece of this proposal, so I think it makes sense to discuss here.

Have the web developer hoist the shared stylesheets out into the global space, to make it more obvious that they're doing something that is not constrained by shadow DOM instead of having this implicit shadow-crossing behavior of populating the module map.

The module map is already global when done via script, and it already supports stylesheets. The only difference with this proposal is that it's being done with markup instead of script. See https://web.dev/articles/css-module-scripts.

@domenic, can you clarify why you're opposed to using the module map for this? Note that this was also discussed in two TPAC breakout sessions, where the discussions included all 3 implementers and people from the WC developer community, and feedback was generally favorable towards the module approach.

It's certainly possible that the specifier attribute is too vague. @robglidden suggested export instead of specifier and import instead of adoptedstylesheets in order to generalize and provide intent (see linked slides here).

Have some new syntax, e.g. globalid="", for populating the global ID map while inside the shadow tree. This seems like a bad idea to me, but so does populating the global module map, and I'd rather we have DOM elements populate the global ID map than the global module map, since they're DOM elements.

The module map is already global when accessed via script (and already supports global stylesheets), so I don't see the advantage of introducing another global map. I would also expect a globalid to work anywhere an IDREF is expected, which might be too broad.

Use some of the more esoteric techniques that people use to forward IDs across shadow boundaries, e.g. for ::part(), or the recent ARIA proposals.

Are you referring to shadowrootreferencetarget? It is somewhat similar, but only allows references to propagate inside the shadow root - https://github.com/WICG/webcomponents/blob/gh-pages/proposals/reference-target-explainer.md, so styles defined in shadow roots couldn't be added to the global map (i.e. it could allow the shadow root to access global styles, but it wouldn't allow shadow roots to define styles that can be used globally).

We also probably don't want to diverge too far from the existing script-based CSS Module Scripts. Requiring dependencies on other features (especially esoteric ones) seems counter to that.

@annevk
Copy link
Member

annevk commented Oct 21, 2024

Note that this was also discussed in two TPAC breakout sessions, where the discussions included all 3 implementers and people from the WC developer community, and feedback was generally favorable towards the module approach.

Having attended at least one of those I don't think that's an accurate characterization. There was quite a bit of confusion as to how this would work, that it was different from everything else we have in HTML to date, and that this would warrant further discussion.

@dandclark
Copy link
Contributor

There were a number of open questions that we need to continue discussing -- thanks @KurtCattiSchmidt for starting to get issues opened for these -- but my impression was that the module-based approach was the one that had the most energy and positive sentiment around it out of the many ideas Kurt presented for solving this problem.

The modules-based approach had gotten particularly positive feedback both during and outside of TPAC from the WebComponents dev community (thanks to folks like @justinfagnani and @Westbrook from sharing their thoughts). And while questions and concerns were raised from implementers, I wasn't picking up on feedback recommending we move away from the overall modules-based approach or pursue a different path.

(I definitely don't want to mischaracterize anyone's position; notes from the sessions can be found here and here.)

@domenic (and others!) it would be great if you can share the specific concerns you have with the module-based approach so we can discuss. In the meantime we plan to continue thinking through the open questions that have already been raised and working on shoring up some of the more hand-wavy areas of the proposal.

@Westbrook
Copy link

The idea that:

<script type="css-module" specifier="/foo.css">
  #content { 
    color: red; 
  }
</script>
<my-element> 
   <template shadowrootmode="open" adoptedstylesheets="/foo.css"> 
     <!-- ... -->
   </template>
</my-element>

Is an almost 100% declarative translation of:

import style from '/foo.css' with { type: 'css' };

class MyElement extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    this.shadowRoot.adoptedStyleSheets = [ style ];
    this.shadowRoot.innerHTML = '...';
  }
}

customElements.define('my-element', MyElement);

Is incredibly exciting to the Web Components Community Group and the greater community made up by its members' various networks. It's seemingly only a few steps away from fully declarative custom elements, which seem just as interesting to implementors as they do to the developing communities served by the hard work those implementors are putting into browsers. The way it implies a future syntax for other module scriptable content types is also quite nice, and quite needed in the content of each (JSON, CSS, HTML, WASM, et al).

If there is anything the WCCG can do to support moving the conversation around this API forward (testing [we've been working on WPT coverage for a number of features as of late and are happy to expand out efforts for such an important API], feedback [point us to the right place], demos [we actually had a series of presentation in the Spring around Declarative Custom Elements concepts, many of which featured the need for an API like this], etc.), please let us know!

@domenic
Copy link
Member

domenic commented Oct 22, 2024

it would be great if you can share the specific concerns you have with the module-based approach so we can discuss.

I think I did? I'll try restating:

  • Allowing modification of the module map so that it contains importable (i.e. non-inline) modules that do not correspond to network resources is unprecedented, and requires much more significant design work. (E.g., interaction with import maps, especially mutable import maps. Or just the mutability of the specifier="" attribute itself!)
  • Doing so only for CSS modules is poor design. Any such solution should be designed for all module types.
  • Reusing the module map for what are not really modules, but instead a global map of constructible stylesheets, is unnecessarily entangling two parts of the platform.
  • This pierces the shadow root boundary, which generally not allowed for declarative features. As you point out, scripts can do it via imperative APIs, but we have not allowed markup inside a shadow root to poison global namespaces so far, and we shouldn't do so here.
  • The global ID map may suffice; I have not seen anyone describe why authors need to be able to reference adopted stylesheets across shadow roots and cannot instead hoist them outward. (E.g. @Westbrook's example would work fine with the global ID map.)

I appreciate that authors find the connection between these technologies exciting. But we have to look at use cases, and how we can accomplish them without overly complicating or special-casing the platform. So far I have not seen a use case that requires modules as a technology; the use case is roughly "declarative adopted stylesheets", which can be accomplished in many other, less complex and less precedent-breaking ways.

I'm sorry that I wasn't in the room where this was discussed at TPAC, but I want to stress that the WHATWG working mode is async-first, and that this is the first thread where this proposal has been presented to the WHATWG community in a way that equitably allows all participants to comment without attending a specific meeting.

@Westbrook
Copy link

I'm not sure a global map would be beneficial to a page level developer achieve their goals in that not populating the module graph with /foo.css (or similar) would prevent <my-other-element> from being able to benefit from the shared code in the CSS module script later in the lifecycle of the page, seemingly?

import style from '/foo.css' with { type: 'css' };

class MyOtherElement extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    this.shadowRoot.adoptedStyleSheets = [ style ];
    this.shadowRoot.innerHTML = '...';
  }
}

customElements.define('my-other-element', MyElement);

While I could envision paths by which a global ID map were bent to the will of the developer in this case, it's hard to see a path that does not require a developer to more heavily rely on tooling in that the ability to convert the standard code of:

import style from '/foo.css' with { type: 'css' };

// elide complexity

this.shadowRoot.adoptedStyleSheets = [ style ];

to something like:

this.shadowRoot.adoptedStyleSheets = [ someMagicMapBoundToWindow.get('foo.css') ];

This wide separation of what is written and what is shipped seems like a lot when the module graph already exists exactly for this sort of responsibility.

On the topic of other module types, some feel that even including them in the explainer is preparing it for derision (due to an even broader expanse of unanswered questions). However, there is specific reference to HTML module scripts in the explainer. I greatly agree that clarifying that the specifier attribute can apply across module types (include JS itself!) and how would be a great way to solidify the benefits of this approach!

@domenic does bring up a question I hadn't seen before about the mutability of the specifier attribute itself.

  • Should it be some sort of magic attribute wherein it can only be set with side effects once? If possible, feels like a cheat code!
  • Is there a path to removing something from the module graph? This would allow it to just be live, but the cascade effects of things no longer having resolved values seems like it opens even more questions.
  • Should it be "write only" to the module graph? Meaning it populates every value that it is ever set to with its contents, were the value not otherwise available in the graph. This does align with some of the questions about possible spamming of the module graph at TPAC.

@KurtCattiSchmidt
Copy link
Author

Good questions @domenic!

Allowing modification of the module map so that it contains importable (i.e. non-inline) modules that do not correspond to network resources is unprecedented, and requires much more significant design work. (E.g., interaction with import maps, especially mutable import maps. Or just the mutability of the specifier="" attribute itself!)

I agree, this is probably the most controversial part of this design. However, it's not decided whether this will not correspond to network requests. See #10711. Regardless of the outcome of that issue though, developers want an alternative that can be done without external CSS files.

Good points on import maps - I will need to do some investigation on how these can interact. Also mutating the specifier attribute is a good call out - this behavior will need to be clarified.

Doing so only for CSS modules is poor design. Any such solution should be designed for all module types.

This doesn't preclude doing so for other types of modules. We can certainly spec other modules in a similar way.

Reusing the module map for what are not really modules, but instead a global map of constructible stylesheets, is unnecessarily entangling two parts of the platform.

The module map is already being used as a global map of constructable stylesheets. See https://web.dev/articles/css-module-scripts.

This pierces the shadow root boundary, which generally not allowed for declarative features. As you point out, scripts can do it via imperative APIs, but we have not allowed markup inside a shadow root to poison global namespaces so far, and we shouldn't do so here.
The global ID map may suffice; I have not seen anyone describe why authors need to be able to reference adopted stylesheets across shadow roots and cannot instead hoist them outward. (E.g. @Westbrook's example would work fine with the global ID map.)
I appreciate that authors find the connection between these technologies exciting. But we have to look at use cases, and how we can accomplish them without overly complicating or special-casing the platform. So far I have not seen a use case that requires modules as a technology; the use case is roughly "declarative adopted stylesheets", which can be accomplished in many other, less complex and less precedent-breaking ways.

I covered quite a few alternatives (including global ID scoping withing shadow DOM) in my TPAC discussion, and why each of them comes with unfortunate tradeoffs - I would encourage you to take a look at my slides. Happy to discuss alternatives here though, in case those tradeoffs are not dealbreakers.

I'm sorry that I wasn't in the room where this was discussed at TPAC, but I want to stress that the WHATWG working mode is async-first, and that this is the first thread where this proposal has been presented to the WHATWG community in a way that equitably allows all participants to comment without attending a specific meeting.

No need to apologize, and agreed, this is the right place to discuss these kinds of concerns.

@domenic
Copy link
Member

domenic commented Oct 23, 2024

Thanks all for the discussion! I'm glad we're able to get into the technical details like this.

@Westbrook: I'm a bit confused by your comment. Before, you were talking about making declarative CSS modules + shadow DOM an easy translation from the imperative versions. Now you are talking about... making one version of imperative CSS modules + shadow DOM into a different imperative version? That doesn't seem related to the goal of this proposal, from what I can tell.

But I'll try to engage anyway:

to something like:

this.shadowRoot.adoptedStyleSheets = [ someMagicMapBoundToWindow.get('foo.css') ];

No "some magic map bound to window" necessary. You can use document.getElementById() to access the global ID map.

How to get from the element to the CSSStyleSheet object depends on some details, discussed below.

To @KurtCattiSchmidt:

I agree, this is probably the most controversial part of this design. However, it's not decided whether this will not correspond to network requests. See #10711. Regardless of the outcome of that issue though, developers want an alternative that can be done without external CSS files.

To be clear, adding more complexity to a feature I'm objecting to, by also adding network request support, doesn't ameliorate my objections to the base feature :)

Good points on import maps - I will need to do some investigation on how these can interact. Also mutating the specifier attribute is a good call out - this behavior will need to be clarified.

I'd encourage you to take a step back and engage with alternatives, as I think they'll be a lot more feasible to reach consensus by sidestepping a lot of these problems.

This doesn't preclude doing so for other types of modules. We can certainly spec other modules in a similar way.

In my experience, module features need to be designed holistically. I wouldn't be comfortable with doing something CSS-specific that changes the fundamental nature of the module system in such a way. It would need to support all existing module types (JS, wasm, CSS, JSON) from day 0 in order to reach consensus.

The module map is already being used as a global map of constructable stylesheets. See https://web.dev/articles/css-module-scripts.

That's not really accurate. (Please try to refer to the spec, instead of third-party articles which can be imprecise.)

The module map is used as a global map of modules. Sometimes those modules can be CSS modules. CSS modules are exposed to JavaScript as CSSStyleSheet objects.

It is not used a as global map of CSSStyleSheet objects. The fact that sometimes CSStyleSheets can be accessed via it, under certain network setups, does not mean it's a good place to insert CSSStyleSheet objects.

I covered quite a few alternatives (including global ID scoping withing shadow DOM) in my TPAC discussion, and why each of them comes with unfortunate tradeoffs - I would encourage you to take a look at my slides. Happy to discuss alternatives here though, in case those tradeoffs are not dealbreakers.

I assume you're referring to slide 20? Here are my takes on the problems identified there. TLDR they are not really problems.

Does not align with script-based adoptedStyleSheets

Script-based version takes an object reference – this is very different than a corresponding attribute that takes ID’s

I can't understand what point this is concretely making. Remember, we need to stay focused on the use cases and requirements (which your slides list). This seems to be an aesthetic preference between objects vs. strings?

Can you give an example of a thing a web developer wants to accomplish where strings vs. objects makes a difference?

If this is about just the logistics of how to assign to adoptedStyleSheets, like in @Westbrook's question above, then my suggestion is:

There are alternatives that are more complex, based off of defining inline CSS module scripts (#7367) and then accessing their default export (#7415). I would not object to going that route, as long as you did the design work for CSS/JSON/WASM all at the same time. But it's much more complex, so since the above seems like it accomplishes your use cases, I would suggest starting there.

(In case it is confusing why I would not object to something based off of inline CSS module scripts, but I do object to the specifier="" design: inline module scripts are an established concept. Giving URL specifiers, as well as a place in the module map, to non-networked resources, is much more disruptive.)

No ability to define styles without applying them to the light DOM

The disabled="" attribute solves this.

Not extensible for other resources

Again, we need to stay focused on the use case, of "declarative adopted stylesheets". Creating over-generic solutions, ahead of the actual use cases, is dangerous.

If you want to expand the requirements to include other resources, gather evidence for the web developer need, and solve all of them at the same time, then I'd be cautious but supportive. But we cannot tackle just CSS, and vaguely hope that our solution will work well for other resources in the future.

@KurtCattiSchmidt
Copy link
Author

KurtCattiSchmidt commented Oct 24, 2024

To be clear, adding more complexity to a feature I'm objecting to, by also adding network request support, doesn't ameliorate my objections to the base feature :)

Good point :) However, the premise of this feature is to allow adoptedStyleSheets without requiring external files. This is what web developers have requested. Regardless of whether this initiates a fetch or not, I do think that we should address that developer need.

In my experience, module features need to be designed holistically. I wouldn't be comfortable with doing something CSS-specific that changes the fundamental nature of the module system in such a way. It would need to support all existing module types (JS, wasm, CSS, JSON) from day 0 in order to reach consensus.

That makes sense. If we do go down this route, I'm happy to incorporate those into the spec.

That's not really accurate. (Please try to refer to the spec, instead of third-party articles which can be imprecise.)
The module map is used as a global map of modules. Sometimes those modules can be CSS modules. CSS modules are exposed to JavaScript as CSSStyleSheet objects.
It is not used a as global map of CSSStyleSheet objects. The fact that sometimes CSStyleSheets can be accessed via it, under certain network setups, does not mean it's a good place to insert CSSStyleSheet objects.

The current behavior of import sheet from './styles.css' with { type: 'css' }; will insert a CSStyleSheet object into the module map. This proposal provides an alternate means of doing that via markup.

Your original post said it was "unnecessarily entangling two parts of the platform" - can you clarify which two parts of the platform you're referring to? I assumed you meant 1) stylesheets and 2) the module map, but this response further confirms that they are already entangled.

I assume you're referring to slide 20? Here are my takes on the problems identified there. TLDR they are not really problems.

The biggest issue with any ID-based approach is that ID's are scoped to shadow roots. This makes any ID-based approach fundamentally incompatible with shadow DOM, as one of shadow DOM's main features is ID isolation. You mentioned earlier about a global ID, which would address this issue, but that comes with its own set of tradeoffs. But I'm happy to discuss those in this thread.

I can't understand what point this is concretely making. Remember, we need to stay focused on the use cases and requirements (which your slides list). This seems to be an aesthetic preference between objects vs. strings?

My slides didn't really cover this, but in-person I mentioned the asymmetrical nature of things like:

foo.adoptedStyleSheets = [some object]; vs
foo.setAttribute("adoptedstylesheets", "some list of id's");

The only existing attribute I can think of that take a list of ID's are the Aria properties like aria-labelledby and aria-describedby, and these handle this issue by having different name for the DOM API that takes object references - ariaLabelledByElements; (currently only supported in WebKit).

That's an option here, but since the DOM API existed first (unlike with aria-labelledby and ariaLabelledByElements), I'm attempting to align with that as much as possible.

Can you give an example of a thing a web developer wants to accomplish where strings vs. objects makes a difference?

It's more of a consistency thing, as I don't see any other API's that behave so differently between the DOM API and HTML attribute. But I might wrong here.

The disabled="" attribute solves this.

By making a stylesheet disabled, it's also disabled when adopted (and thus not applied), so I don't see how it solves this issue. See codepen here.

Using a different type attribute on the <style> tag as proposed here does address this though, as the only valid types are 1) empty and 2) "text/css" https://html.spec.whatwg.org/#update-a-style-block.

Again, we need to stay focused on the use case, of "declarative adopted stylesheets". Creating over-generic solutions, ahead of the actual use cases, is dangerous.

If you want to expand the requirements to include other resources, gather evidence for the web developer need, and solve all of them at the same time, then I'd be cautious but supportive. But we cannot tackle just CSS, and vaguely hope that our solution will work well for other resources in the future.

This is great advice, much appreciated.

  • Most broadly stated the developer use case is "devs want to share a stylesheet instance across shadows, without script and without a network request".

  • Zooming out and looking at things holistically, there's other similar instances of this problem like "devs want to reference a centrally-defined chunk of HTML and use it to populate the contents of multiple shadows, without script and without a network request". Together with the use case in the previous bullet these could be generalized to say "developers want to define a resource in a central place and apply it to shadows without duplicating unnecessarily, and without network requests or script". Plugging into modules seems to be the best path forward for solving this holistically, for multiple content types.

  • We think the current usage of adoptedStyleSheets has the cleanest mapping to the declarative approach, and the strongest developer demand, so that's where we started. We have example code of how this could work with HTML in our explainer and will flesh that out in more detail, and will also think through how this can work for other module types where appropriate.

@domenic
Copy link
Member

domenic commented Oct 24, 2024

The current behavior of import sheet from './styles.css' with { type: 'css' }; will insert a CSStyleSheet object into the module map.

This is not accurate; I wrote out the actual specified behavior in more detail above.

But, this kind of discussion about exactly what the spec says is not that important, since it's not focused on the use cases and how we can solve them. So I'm happy to drop this.

Your original post said it was "unnecessarily entangling two parts of the platform" - can you clarify which two parts of the platform you're referring to?

I mean, inline HTML markup (whether modules or <style>) and the external-resource-based module map.

The biggest issue with any ID-based approach is that ID's are scoped to shadow roots. This makes any ID-based approach fundamentally incompatible with shadow DOM, as one of shadow DOM's main features is ID isolation. You mentioned earlier about a global ID, which would address this issue, but that comes with its own set of tradeoffs. But I'm happy to discuss those in this thread.

Please explain this issue. In particular, I've asked repeatedly in this thread for cases where the stylesheet cannot be hosted outside the shadow DOM. Nobody has provided one so far, and all examples (e.g. in the presentation, or in #10673 (comment)) locate the stylesheet outside of the shadow DOM.

Maybe there's a misconception that you cannot refer to ID-based elements outside the shadow DOM, from inside the shadow DOM? That's not correct. The shadow DOM design constraints are:

  • Cannot influence global namespaces (e.g. the global ID space, or the global module map) using markup from inside the shadow DOM.
  • Markup outside the shadow DOM cannot see into the shadow DOM.

There's no constraint which prevents markup inside the shadow DOM from seeing outside the shadow DOM.

My slides didn't really cover this, but in-person I mentioned the asymmetrical nature of things like:

Thanks for explaining. It sounds like we agree that this isn't a requirement.

By making a stylesheet disabled, it's also disabled when adopted (and thus not applied), so I don't see how it solves this issue. See codepen here.

That is why I included the line in my proposal about modifying the behavior of the adoptedStyleSheets setter.

This is great advice, much appreciated.

[3 bullets]

Thanks so much for summarizing the use cases and zooming out. I agree with everything you wrote here, even including "Plugging into modules seems to be the best path forward for solving this holistically, for multiple content types"!

The part where we diverge is the use of the module map, vs. using inline modules. So my suggested decision tree would be:

  • Decide whether you're willing to tackle all module types at once, or want to focus on CSS.
  • If only CSS: work on something involving <style> elements, like my suggestion with disabled="" above, or yours with a different type="".
  • If all module types: work on something based on inline modules, not modifying the module map.

@justinfagnani
Copy link

Thanks for the questions @domenic

I meant to reply earlier, so I'll go back to your first list

it would be great if you can share the specific concerns you have with the module-based approach so we can discuss.

I think I did? I'll try restating:

  • Allowing modification of the module map so that it contains importable (i.e. non-inline) modules that do not correspond to network resources is unprecedented

I think these modules should generally correspond to importable modules. We're trying to figure out how to serialize a CSS module import that's been adopted to a shadow root. The URL that the inline module uses should match the resolved URL of the CSS module so that the CSS import shares the module map entry.

  • Doing so only for CSS modules is poor design. Any such solution should be designed for all module types.

I agree, and personally want this feature to be a general module inlining feature. I know of use cases for inline JS modules that are sill importable by external modules.

  • Reusing the module map for what are not really modules, but instead a global map of constructible stylesheets, is unnecessarily entangling two parts of the platform.

The should really be modules. It shouldn't be a map of stylesheets, but the existing map of modules, some of which are CSS modules.

  • This pierces the shadow root boundary, which generally not allowed for declarative features. As you point out, scripts can do it via imperative APIs, but we have not allowed markup inside a shadow root to poison global namespaces so far, and we shouldn't do so here.

Working across shadow roots is a critical feature. Without that this whole thing doesn't solve anything wrt the current situation where you have to duplicate all stylesheets used in shadow roots. The point is to deduplicate all the stylesheets that have to be repeated today.

  • The global ID map may suffice; I have not seen anyone describe why authors need to be able to reference adopted stylesheets across shadow roots and cannot instead hoist them outward. (E.g. @Westbrook's example would work fine with the global ID map.)

It's not possible to hoist these modules because you only discover them as you render components. You don't know ahead of time which components will be rendered. You could hook module loading on the server and emit every CSS module that's imported in the graph, but that could be wildly inefficient. Highly complex dynamic pages may only render a small fraction of the components in the graph for any given URL.

For example, let's say you have root component A that dynamically renders either child component B or C, each of which dynamically render D or E (for B), or F or G (for C). On the server you don't know if you're going to render B or C until you run the actual logic for A, and on down the tree.

So you render this:

<x-a>
  <template shadowrootmode="open">
    <x-c>

And here is where you need to emit the styles for <x-c>. If that style isn't usable is other shadow roots, then you have to emit it again for the next <x-c>. To emit the styles only once, but also hoist them, you'd have to hold onto them until the end of any top-level shadow roots, then emit them, which would be really bad.

@robglidden
Copy link

Here is a thought-exercise repo that demos using import maps, which already in Chrome support CSS modules from files and (could?) sidestep or at least scope the specifier attribute issue, while also providing the necessary capabilities of a mutable adopt attribute, modifiable stylesheets, and a working import().

<script type="importmap">
{
    "imports": {
        "dashedstyles": "./dashedstyles.css",
        "solidstyles": "./solidstyles.css",
        "toggleBorderSizes": "./toggleBorderSizes.js"
    }
}
</script>

To me, the "simple" approach of using a script or style tag's id or global id to assign styles to adoptedStyleSheets could simplify the "export" side of this proposal by avoiding the module map altogether.

But it would put more complexity on the "import" side, because to build out features like a mutable importing attribute and modifying the underlying CSSStylesheet associated with an exported style would seem to require some kind of parallel tracking and referencing system.

So +1 to "we cannot tackle just CSS, and vaguely hope that our solution will work well for other resources in the future" and we should do the "design work for CSS/JSON/WASM all at the same time".

To me, import maps, and the multiple import maps work already well underway hint that polymorphic URL module specifiers would be both confusing to web developers and difficult to implement.

It might be a more reasonable tradeoff to have a constraint similar to what is already on import maps that declarative inline modules must be must be declared and processed before any <script> elements that import modules using specifiers declared in the map.

Although import maps don't support inline modules (at least today, though this thought exercise implies that might be worth considering), they are at least declarative-ish. And their preprocessor-ish nature parallels in part the need for declarative shadow DOMs to be HTML-parsed early. And like bare specifiers they provide a convenient way to reference, add, modify, and remove adopted stylesheets.

From a Web developer point of view, this would be simpler than the above sounds, as the gif in the repo show:

  • file-based CSS modules could be referenced, adopted, updated, and readopted without any change to the module system
  • inline CSS modules would need to be declared early, like import maps (or through import maps?)
  • no confusing polymorphic specifier URL

Resources other than CSS do not have an exact equivalent to adoptStylesheets, so that needs to be thought through also.

@o-t-w
Copy link

o-t-w commented Oct 29, 2024

For anybody not already following along, there is a parallel discussion about this same topic here: w3ctag/design-reviews#1000

@nolanlawson
Copy link

nolanlawson commented Nov 3, 2024

This is a really exciting proposal! One potential issue I want to raise is with polyfills: could there be a declarative way to detect support, which would allow for falling back to <link rel=stylesheet>?

I ask because a web author using this API will have to support older browsers for some time, and although you could imagine injecting <script>s to detect support, this kind of defeats the purpose of a JS-less API (e.g. FOUC).

The analogy here would be with <script nomodule> – having a way to detect browsers that don't support modules is very powerful for emitting the ideal scripts for both older and newer browsers. Just a sketch:

<my-element> 
   <template shadowrootmode="open" adoptedstylesheets="/foo.css"> 
       <link rel="stylesheet" href="/foo.css" noadoptedstylesheets> <!-- no-op on newer browsers -->
   </template>
</my-element>

@niutech
Copy link

niutech commented Nov 3, 2024

In my opinion, putting CSS styles inside <script type="css-module" specifier="/foo.css">...</script> is confusing both for web developers and for syntax highlighters in code editors. I'd propose a simpler solution:

Just use a set of global <style id="foo" type="css-module">...</style> tags in <head> (reusing the type property; if type != "text/css" it shouldn't be applied to the document), then reference them using <link rel="stylesheet" href="#foo"> from inside the Declarative Shadow DOM. Just modify the HTML spec so that if the <link rel="stylesheet">'s href property starts with a #, e.g. #foo, then don't make a request but refer to the contents of the <style id="foo"> element if it exists.

@socketdigital
Copy link

Using the # symbol as a dedicated inline module specifier could also untangle the race-conditiony dual-use '/foo.css' specifier in the original proposal.

In other words, add a fourth type of specifier for inline modules to the three current types of specifiers (relative, absolute, and bare).

This would work with dynamic and static imports and import maps, and be consistent with the existing module system. And open up the idea of inline modules to a broader range of use cases.

And the # symbol is already used to mean the id of an element:

  • as a fragment identifier in URLs,
  • in CSS selectors to select elements by their id attribute
  • in an href attribute on a web page to link to an anchor within the same page

The # symbol as a reference to a light DOM element by id could also be used for the simpler approach to adoptable style sheets by light DOM element ids.

@justinfagnani
Copy link

In my opinion, putting CSS styles inside <script type="css-module" specifier="/foo.css">...</script> is confusing both for web developers and for syntax highlighters in code editors.

I disagree and think it's more consistent for a feature that inlines modules. All inlined modules would be represented with a <script> tag regardless of what type of module it is. I think this is a simpler alternative than a tag per module type.

Just use a set of global <style id="foo" type="css-module">...</style> tags in <head>

This doesn't work because it would require you to know all the styles you want to inline before you emit any of your components. You'd have to emit all possible styles, even if you only use a subset of them for the page.

@niutech
Copy link

niutech commented Nov 8, 2024

@justinfagnani What's the difference between <script type="css-module" specifier="/foo.css"> and <style type="css-module" id="foo"> in practice? Both of them should be included along with a corresponding web component. The backend (or SSG) should be responsible for injecting only the relevant CSS styles and web components into HTML documents.

@rniwa
Copy link

rniwa commented Nov 8, 2024

An important consideration is what we're gonna do with HTML modules. Using script element there isn't great because then you can't have nested HTML modules (script elements can't be nested).

@justinfagnani
Copy link

An important consideration is what we're gonna do with HTML modules. Using script element there isn't great because then you can't have nested HTML modules (script elements can't be nested).

Ah, true. And there's no way to escape a </script> tag inside a script, right?

@robglidden
Copy link

I think HTML modules and recently here, are due for a back-to-basics TAG design re-review in light of modern developments of import attributes, import map constraints and DSD as well as this current discussion of inline modules and declarative importing.

Even back in 2017 (see), an HTML module could be as simple as a document fragment. I sometimes find that a good starting point, particularly given CSS and JSON import attributes solve part of multi-type module packaging as originally envisioned for HTML modules.

@aluhrs13
Copy link

Just a small update here - We've made some additions to the explainer to start to cover some of the things we're discussing here, and we're also looking into TAG's suggestion of @sheet as a potential direction.

@robglidden
Copy link

Good to see in the updated explainer consideration of import attributes like SVG.

I note that this already works from inside a shadow DOM:

<icon-button>
  <template shadowrootmode="open">
    <button>
      <svg>
        <use href="./icons.svg#folder-tree"></use>
      </svg>
    </button>
  </template>
</icon-button>

What you can't do now is <use href="#folder-tree"></use> to a light DOM svg element from within a shadow DOM like you can elsewhere.

Being able to use <use href="#folder-tree"></use> as in the explainer would be better, though apparently at the cost of cloning? And with the specifier technique in the current explainer, ./icons.svg would have a different meaning depending on where used, or introduce backwards incompatibility.

Using a reference to a light DOM element or (inline) specifier might be better, ala <use href="##folder-tree"></use>.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
addition/proposal New features or enhancements needs implementer interest Moving the issue forward requires implementers to express interest stage: 1 Incubation topic: script topic: shadow Relates to shadow trees (as defined in DOM)
Development

No branches or pull requests