Skip to content

Declarative CSS Modules#11687

Open
KurtCattiSchmidt wants to merge 23 commits intowhatwg:mainfrom
KurtCattiSchmidt:css-modules-firstpr
Open

Declarative CSS Modules#11687
KurtCattiSchmidt wants to merge 23 commits intowhatwg:mainfrom
KurtCattiSchmidt:css-modules-firstpr

Conversation

@KurtCattiSchmidt
Copy link
Copy Markdown
Contributor

@KurtCattiSchmidt KurtCattiSchmidt commented Sep 24, 2025

Adds support for Declarative CSS Modules via <style type="module" specifier="specifiername">. shadowrootadoptedstylesheets is handled in this PR: #12339

(See WHATWG Working Mode: Changes for more details.)

Addresses #10673


/acknowledgements.html ( diff )
/indices.html ( diff )
/infrastructure.html ( diff )
/obsolete.html ( diff )
/scripting.html ( diff )
/semantics.html ( diff )

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR introduces declarative CSS modules support to HTML, allowing CSS to be imported as modules through <style> elements with a specifier attribute and <template> elements with a shadowrootadoptedstylesheets attribute.

  • Adds a specifier attribute to <style> elements that creates module import maps for CSS content
  • Introduces a shadowrootadoptedstylesheets attribute for <template> elements to declaratively adopt CSS modules
  • Implements algorithms for creating declarative CSS module scripts and stylesheet adoption
Comments suppressed due to low confidence (5)

source:1

  • Missing attribute name in IDL definition. Should be [SameObject, PutForwards=value, Reflect] readonly attribute DOMString <dfn attribute for="HTMLStyleElement" data-x="dom-style-specifier">specifier</dfn>; to match the pattern used for other attributes in this interface.
<!-- -*- mode: Text; fill-column: 100 -*- vim: set textwidth=100 :

source:1

  • Grammar error: 'is defines' should be 'defines' - remove the word 'is'.
<!-- -*- mode: Text; fill-column: 100 -*- vim: set textwidth=100 :

source:1

  • Grammar error: 'appended with the of the' should be 'appended with the' - remove the duplicate 'the of'.
<!-- -*- mode: Text; fill-column: 100 -*- vim: set textwidth=100 :

source:1

  • Logic error: The algorithm references <var>specifier</var> but this variable is not defined in the algorithm. It should reference the value of the shadowrootadoptedstylesheets attribute instead.
<!-- -*- mode: Text; fill-column: 100 -*- vim: set textwidth=100 :

source:1

  • Incorrect data-x reference: Should be data-x=\"attr-style-specifier\" not data-x=\"attr-style-blocking\" for the specifier attribute row.
<!-- -*- mode: Text; fill-column: 100 -*- vim: set textwidth=100 :

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

Comment thread source Outdated
Comment thread source
Comment thread source Outdated
Comment thread source Outdated
Comment thread source Outdated
Comment thread source Outdated
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (9)

source:1

  • The IDL attribute should be named to match the content attribute. The content attribute is specifier but the IDL should use camelCase convention: [SameObject, PutForwards=value, Reflect] readonly attribute DOMString specifier; should have a data-x attribute defining the DOM property name.
<!-- -*- mode: Text; fill-column: 100 -*- vim: set textwidth=100 :

source:1

  • The shadowrootadoptedstylesheets attribute is listed twice in the content attributes section for the template element. This duplication should be removed.
<!-- -*- mode: Text; fill-column: 100 -*- vim: set textwidth=100 :

source:1

  • The shadowrootadoptedstylesheets attribute is listed twice in the content attributes section for the template element. This duplication should be removed.
<!-- -*- mode: Text; fill-column: 100 -*- vim: set textwidth=100 :

source:1

  • The algorithm step is missing proper HTML structure. It should end with </li> and the nested <ol> should be properly closed with </ol>.
<!-- -*- mode: Text; fill-column: 100 -*- vim: set textwidth=100 :

source:1

  • The variable moduleScript is referenced but never defined in this algorithm. This should likely be the current module script or settings object context.
<!-- -*- mode: Text; fill-column: 100 -*- vim: set textwidth=100 :

source:1

  • These variable assignments are missing closing </p> tags. Each should end with </p></li>.
<!-- -*- mode: Text; fill-column: 100 -*- vim: set textwidth=100 :

source:1

  • Missing spaces after commas in the parameter list. Should be <var>fetchClient</var>, <var>destination</var>, <var>options</var>, <var>settingsObject</var>, <var>referrer</var> for consistency.
<!-- -*- mode: Text; fill-column: 100 -*- vim: set textwidth=100 :

source:1

  • Incorrect data-x reference in the table. Line 148289 should reference data-x=\"element-template\" or similar, not data-x=\"attr-template-shadowrootclonable\".
<!-- -*- mode: Text; fill-column: 100 -*- vim: set textwidth=100 :

source:1

  • Incorrect data-x reference in the table. Line 148363 should reference data-x=\"element-style\" instead of data-x=\"attr-style-blocking\".
<!-- -*- mode: Text; fill-column: 100 -*- vim: set textwidth=100 :

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

Copy link
Copy Markdown
Contributor

@dandclark dandclark left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review in progress, sharing feedback so far.

Comment thread source Outdated
Comment thread source Outdated
Comment thread source Outdated
Comment thread source Outdated
Comment thread source Outdated
Comment thread source Outdated
Comment thread source Outdated
Comment thread source Outdated
Comment thread source Outdated
Copy link
Copy Markdown

@mhochk mhochk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM (No permissions to actually Approve)

Copy link
Copy Markdown
Contributor

@dandclark dandclark left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is coming together nicely. I think the biggest thing still to figure out is to prevent a given <style type=module> from being processed twice. I'm about to head out on leave so I'm going to Approve this since I'm supportive of the direction and I trust that the remaining open issues will be handled appropriately.

Comment thread source
<ol>
<li><p>Let <var>element</var> be the <code>style</code> element.</p></li>

<li><p>If <var>element</var> is not <span>connected</span>, then return.</p></li>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess this is also the point where we'd also check a new equivalent of the already started flag to ensure a given <style type=module> only ever gets processed once?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I'll have to think about this a little more. I will likely bring this up soon with the WHATWG.

Comment thread source Outdated
Comment thread source Outdated
of the value of the <span data-x="attr-style-specifier">specifier attribute</span> and a value of
<var>styleDataURI</var>.</p></li>

<li><p><span>Create an import map parse result</span> with <var>input</var> as <var>jsonString</var>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Create an import map parse result can throw -- do we need to handle any of those cases? The one I particularly had in mind to watch for is does it throw when a given specifier is invalid? If not, do we need to handle an invalid specifier some other way?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question. For now I added a step to look at the importMapParseResult's error to rethrow and to continue if that happens. In practice, we probably want to log something in the console, but this might be enough for the spec.

Comment thread source Outdated
Comment thread source Outdated
Comment thread source Outdated
Comment thread source Outdated
@zcorpan
Copy link
Copy Markdown
Member

zcorpan commented Oct 9, 2025

You can include "Fixes #10673" in the OP so that there's a link from the issue in GitHub's UI. (Also should be in the commit message when squashing.)

@KurtCattiSchmidt
Copy link
Copy Markdown
Contributor Author

@annevk - I believe the last two commits addressed all of the issues you brought up this morning, so please take a look when you get a chance. And also let me know if I missed anything else you mentioned today.

@annevk
Copy link
Copy Markdown
Member

annevk commented Oct 13, 2025

I think the most substantive issue I raised was not addressed: #10673 (comment). I think we either need something like blob: URLs or allow the module map to contain element references. Not sure.

@KurtCattiSchmidt
Copy link
Copy Markdown
Contributor Author

I think the most substantive issue I raised was not addressed: #10673 (comment). I think we either need something like blob: URLs or allow the module map to contain element references. Not sure.

@annevk - I just pushed an update that switches the dataURI to a Blob URI. I think this approach is much simpler than expanding the module map to contain element references.

I've been thinking about the Blob approach and haven't come up with any major reasons not to go this route. The lifetime of the Blob object is an interesting one, and it would be nice to give developers a way to revoke the Blob URL. I don't think removing the <style> tag is a good method anymore though, because you would expect that re-inserting it would re-activate it and Blob URL's cannot be reactivated after revoking them. So maybe exposing the Blob URL somewhere is a better option.

Comment thread source Outdated
Comment thread source Outdated
@KurtCattiSchmidt
Copy link
Copy Markdown
Contributor Author

@keithamus - here is the split PR with just the declarative modules

@KurtCattiSchmidt KurtCattiSchmidt added the agenda+ To be discussed at a triage meeting label Mar 11, 2026
@noamr
Copy link
Copy Markdown
Collaborator

noamr commented Mar 12, 2026

I still think that we can have declarative CSS modules with URLs before we go down the rabbit hole of supporting them in inline styles with something like specifier.

The main issue with re-importing CSS URLs in shadow DOM is that they are duplicate.
We can support de-duping them in module-style regardless of supporting them in inline styles, e.g.:

<script type=importmap>
  { "mytheme": { type: "css", href: "theme.css" } }
</script>
<my-element>
  <template shadowrootadoptedstylesheets="mytheme">...</theme>
</my-element>

Or something like:

<script type=importmap>
  { "mytheme": { type: "css", href: "theme.css" } }
</script>
<my-element>
  <link rel=stylesheetmodule href="theme.css">
  <!-- or -->
  <link rel=stylesheetmodule href="mytheme">
</my-element>

This decouples the issue of deduping stylesheet references from the issue of importing inline styles.
The former is a stylesheet-specific problem and the latter, while valid, relates to script modules as well, while this proposal is stylesheet specific.

@KurtCattiSchmidt
Copy link
Copy Markdown
Contributor Author

I still think that we can have declarative CSS modules with URLs before we go down the rabbit hole of supporting them in inline styles with something like specifier.

Thanks @noamr. I've recently split the URL version into its own explainer, see https://github.com/MicrosoftEdge/MSEdgeExplainers/blob/main/ShadowDOMAdoptedStyleSheets/explainer.md.

The main issue with re-importing CSS URLs in shadow DOM is that they are duplicate. We can support de-duping them in module-style regardless of supporting them in inline styles, e.g.:
...
This decouples the issue of deduping stylesheet references from the issue of importing inline styles. The former is a stylesheet-specific problem and the latter, while valid, relates to script modules as well, while this proposal is stylesheet specific.

This proposal creates an import map entry under-the-hood for the specifier attribute on <style>, mapping to a generated Blob URL so there's no duplication involved for a given specifier. This approach could easily be expanded to <script> for script and JSON modules. There's nothing in this proposal that needs to be limited to stylesheets. Happy to chat about this at the sync tomorrow of you're available.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

8 participants