Skip to content

Commit b1fc050

Browse files
Add Explainer for Link Tag Module Imports (#1373)
Initial explainer document as a prerequisite for filing issues based on the discussion with working group members and [shared document](https://docs.google.com/document/d/1SkHwxAIBW5I3uqnmmov4D71ZPbj9woouj3RdPqd3X1w/edit?tab=t.0).
1 parent c27150e commit b1fc050

2 files changed

Lines changed: 300 additions & 0 deletions

File tree

LinkTagModuleImport/explainer.md

Lines changed: 299 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,299 @@
1+
# Style Module Imports via `<link>` Elements
2+
3+
## Authors
4+
5+
- Kurt Catti-Schmidt
6+
7+
## Participate
8+
9+
- [Issue tracker](https://github.com/MicrosoftEdge/MSEdgeExplainers/labels/LinkTagImport)
10+
11+
## Status of this Document
12+
13+
This document is intended as a starting point for engaging the community and
14+
standards bodies in developing collaborative solutions fit for standardization.
15+
As the solutions to problems described in this document progress along the
16+
standards-track, we will retain this document as an archive and use this section
17+
to keep the community up-to-date with the most current standards venue and
18+
content location of future work and discussions.
19+
20+
- This document status: **Active**
21+
- Expected venues: [WHATWG](https://whatwg.org/),
22+
[CSS Working Group](https://www.w3.org/Style/CSS/)
23+
- Current version: this document
24+
25+
## Introduction
26+
27+
Modern web development practices have converged towards building reusable
28+
components instead of building monolithic documents. Technologies such as
29+
Shadow DOM allow for style isolation between components, but in practice this
30+
isolation can lead to duplication and inefficiencies when styling components.
31+
32+
CSS module scripts let JavaScript import a stylesheet as a `CSSStyleSheet`
33+
object that can be applied to multiple shadow roots. Import maps can
34+
declaratively control how the module specifiers used by those JavaScript
35+
imports resolve, but the platform currently requires imperative JavaScript to
36+
apply the resulting stylesheet to each tree scope.
37+
38+
This explainer proposes a declarative way to resolve a module specifier, fetch
39+
the result as a CSS module, and apply the resulting shared stylesheet to
40+
multiple tree scopes using the `<link>` element.
41+
42+
## Goals
43+
44+
- Allow developers to declaratively apply the same CSS module stylesheet to
45+
multiple tree scopes.
46+
- Allow the CSS to reside in an externally cacheable resource while reusing the
47+
CSS module instance for the same resolved URL and module type within the
48+
relevant module map.
49+
- Avoid patterns that would cause measurable performance regressions, such as
50+
stylesheet duplication that cannot be deduplicated.
51+
52+
## Non-goals
53+
54+
- Exporting declarative styles (addressed in
55+
[Declarative CSS Modules](../ShadowDOM/explainer.md)).
56+
- Making module `<link rel="stylesheet">` elements identical in every respect
57+
to classic `<link rel="stylesheet">` elements, `<link rel="modulepreload">`
58+
elements, `<style>` elements, or `adoptedStyleSheets` usage.
59+
60+
## Proposal: Module `<link rel="stylesheet">` Elements
61+
62+
We propose a new module mode for `<link rel="stylesheet">`, selected by
63+
`type="module"`. In this mode, the value of `href` is resolved as a module
64+
specifier through import map processing, using the module
65+
`<link rel="stylesheet">` element's base URL. The resolved URL is then imported
66+
as a CSS module. Once the CSS module has loaded, its styles are applied to the
67+
module `<link rel="stylesheet">` element's tree scope.
68+
69+
For clarity, this explainer calls the existing form a "classic
70+
`<link rel="stylesheet">` element" and the proposed form a "module
71+
`<link rel="stylesheet">` element."
72+
73+
```html
74+
<script type="importmap">
75+
{
76+
"imports": {
77+
"foo": "https://example.com/foo.css"
78+
}
79+
}
80+
</script>
81+
<my-element>
82+
<template shadowrootmode="open">
83+
<link rel="stylesheet" type="module" href="foo">
84+
<p>Inside Shadow DOM</p>
85+
</template>
86+
</my-element>
87+
```
88+
89+
Here, `https://example.com/foo.css` contains the following CSS:
90+
91+
```css
92+
p { color: blue; }
93+
```
94+
95+
With this functionality, the text "Inside Shadow DOM" will be styled blue. The
96+
value of the `href` attribute is resolved as a module specifier using the import
97+
map, `https://example.com/foo.css` is fetched as a CSS module, and the resulting
98+
stylesheet is applied to the shadow root. The shared `CSSStyleSheet` appears in
99+
the shadow root's `styleSheets` collection. For more details on why
100+
`styleSheets` is used instead of `adoptedStyleSheets`, see the
101+
[dedicated section](#imported-stylesheet-appears-in-stylesheets) on this
102+
subject.
103+
104+
### Underlying Stylesheet Is Shared Between Tree Scopes
105+
106+
Within a given module map, module `<link rel="stylesheet">` elements whose
107+
`href` values resolve to the same URL use the same module map entry, keyed by
108+
that URL and the CSS module type, and apply the same `CSSStyleSheet` object.
109+
110+
The following example applies one module stylesheet to the root document tree
111+
and to two distinct shadow roots:
112+
113+
```html
114+
<html>
115+
<head>
116+
<script type="importmap">
117+
{
118+
"imports": {
119+
"foo": "https://example.com/foo.css"
120+
}
121+
}
122+
</script>
123+
<link rel="stylesheet" type="module" href="foo">
124+
</head>
125+
<body>
126+
<p>Text in the document tree</p>
127+
<first-element>
128+
<template shadowrootmode="open">
129+
<link rel="stylesheet" type="module" href="foo">
130+
<p>Inside the first shadow root</p>
131+
</template>
132+
</first-element>
133+
<second-element>
134+
<template shadowrootmode="open">
135+
<link rel="stylesheet" type="module" href="foo">
136+
<p>Inside the second shadow root</p>
137+
</template>
138+
</second-element>
139+
</body>
140+
</html>
141+
```
142+
143+
As a result, modifying the shared stylesheet updates every tree scope to which
144+
it is applied. Once all three module `<link rel="stylesheet">` elements have
145+
loaded successfully, executing the following script will resolve to the
146+
existing CSS module instance in the document's module map:
147+
148+
```js
149+
const foo = (await import("foo", { with: { type: "css" } })).default;
150+
foo.replaceSync("p { color: green; }");
151+
```
152+
153+
The import will reuse the existing module map entry without initiating another
154+
fetch and will update all of the text in the example to green because each
155+
module `<link rel="stylesheet">` element applies the same underlying
156+
`CSSStyleSheet` object to each tree scope.
157+
158+
This capability is not possible with classic `<link rel="stylesheet">`
159+
elements, where each `<link>` element has its own associated stylesheet.
160+
The proposal intentionally extends the stylesheet association model: each
161+
module `<link rel="stylesheet">` element associates the same `CSSStyleSheet`
162+
object with its tree scope, and that object appears in each scope's
163+
`styleSheets` collection.
164+
165+
### Imported Stylesheet Appears in `styleSheets`
166+
167+
Although the underlying `CSSStyleSheet` object is shared, it is deliberately
168+
exposed through `styleSheets` instead of `adoptedStyleSheets`. Exposing
169+
declaratively linked sheets through `adoptedStyleSheets` would allow
170+
script to remove or reorder entries independently of the corresponding
171+
module `<link rel="stylesheet">` elements, breaking synchronization between DOM
172+
order and the applied stylesheet list. The presence and state of qualifying
173+
module `<link rel="stylesheet">` elements instead control membership in the
174+
read-only `styleSheets` collection, and their tree order controls the order of
175+
its entries. This distinction concerns the mutability of the collection's
176+
membership; the shared `CSSStyleSheet` object itself remains mutable, as shown
177+
in the examples above. This proposal therefore extends the CSSOM definition of
178+
which sheets are represented by `styleSheets`, introducing the concept of a
179+
DOM-associated constructed stylesheet.
180+
181+
### Compatibility With Classic `<link rel="stylesheet">` Element Behaviors
182+
183+
Adding module imports to `<link rel="stylesheet">` introduces both similarities
184+
to and differences from classic `<link rel="stylesheet">` elements.
185+
186+
This section covers several broad categories of similarities and differences
187+
between module and classic `<link rel="stylesheet">` elements, each of which
188+
has additional implications.
189+
190+
For a full list of differences and discussion of the options, see the
191+
[planning document](https://docs.google.com/document/d/1SkHwxAIBW5I3uqnmmov4D71ZPbj9woouj3RdPqd3X1w).
192+
193+
#### Many Fundamental `HTMLLinkElement` Behaviors Apply to Module Elements
194+
195+
The `<link>` element supports many fundamental behaviors that will also apply
196+
to module imports, creating similarities between classic and module
197+
`<link rel="stylesheet">` elements.
198+
199+
For instance, the `nonce` attribute and the `onload` and `onerror` event
200+
handlers will apply to module `<link rel="stylesheet">` elements.
201+
202+
#### Some Classic `<link rel="stylesheet">` Element Behaviors Do Not Apply to Module Elements
203+
204+
However, not all features supported by classic `<link rel="stylesheet">`
205+
elements apply in the same way to module `<link rel="stylesheet">` elements.
206+
Several fundamental differences cause their behaviors to diverge.
207+
208+
##### Constructed Stylesheets Have Different Behaviors
209+
210+
Because this proposal builds on existing CSS module script imports, the
211+
stylesheet associated with each module `<link rel="stylesheet">` element is
212+
constructed. This results in behavior that differs from classic
213+
`<link rel="stylesheet">` elements in several ways:
214+
215+
- CSS module scripts do not support `@import` CSS rules.
216+
- A constructed stylesheet always has a `null` `ownerNode`.
217+
- An empty-prelude `@scope` CSS rule normally derives its scoping root from the
218+
stylesheet's `ownerNode`. A constructed stylesheet's `null` `ownerNode`
219+
cannot identify the module `<link rel="stylesheet">` element's parent, and
220+
thus CSS rule matching will differ in this scenario from a classic
221+
`<link rel="stylesheet">` element.
222+
223+
In each of these cases, module `<link rel="stylesheet">` elements will apply
224+
the existing constructed stylesheet behavior, rather than the behaviors of
225+
classic `<link rel="stylesheet">` elements.
226+
227+
##### The Difference in Cardinality Changes the Behavior of Some Attributes
228+
229+
Classic `<link rel="stylesheet">` elements have a one-to-one association with
230+
their `CSSStyleSheet` objects. This proposal deliberately allows multiple
231+
module `<link rel="stylesheet">` elements to share one underlying
232+
`CSSStyleSheet`. As a result, attributes such as `media` and `title` cannot be
233+
mapped directly to the shared stylesheet because each module
234+
`<link rel="stylesheet">` element can apply the same `CSSStyleSheet` object
235+
with different per-element state. There are several potential ways to handle
236+
this scenario, including ignoring these attributes entirely (requiring them to
237+
be set directly on the `CSSStyleSheet` object imperatively),
238+
first-defined-wins, or last-defined-wins, each of which comes with tradeoffs.
239+
240+
##### Module Fetches Are Stricter Than Classic Fetches
241+
242+
Fetches for classic `<link rel="stylesheet">` elements and CSS module scripts
243+
differ. A classic `<link rel="stylesheet">` element creates a potential-CORS
244+
request that uses `no-cors` mode by default; its `crossorigin` attribute can opt
245+
into CORS. Module script requests use `cors` mode, so a cross-origin response
246+
must pass a CORS check. Their decoding also differs. CSS loaded by classic
247+
`<link rel="stylesheet">` elements can use encoding information including a
248+
response-provided encoding, a recognized leading byte sequence that resembles
249+
an `@charset` declaration, or a legacy environment encoding. Module script
250+
responses are always decoded as UTF-8. In essence, module
251+
`<link rel="stylesheet">` element fetches will inherit the stricter module
252+
script fetch semantics, with no option to loosen these restrictions back to the
253+
classic `<link rel="stylesheet">` element fetch behaviors.
254+
255+
## Considered Alternatives
256+
257+
1. [Declarative CSS Modules](../ShadowDOM/explainer.md) are another mechanism
258+
for sharing styles between Declarative Shadow DOM and the document tree
259+
without JavaScript. That proposal introduces `shadowrootadoptedstylesheets`,
260+
which serves a similar purpose. Feedback from working group participants
261+
favors a `<link>`-based approach using `styleSheets` over a new attribute
262+
that uses `adoptedStyleSheets`.
263+
2. [Local References In Link Rel](../LocalReferenceLinkRel/explainer.md) allow
264+
shadow roots to reference stylesheet definitions that are visible through
265+
tree-scoped lookup. They do not provide module-specifier resolution or allow
266+
a stylesheet definition inside one shadow root to be exported to unrelated
267+
roots.
268+
3. CSS-encoded data URI references in `<link>` elements. This approach avoids
269+
some of the issues with 2), but data URI encoding results in poor developer
270+
ergonomics.
271+
272+
## Open Issues
273+
274+
1. How should behavior be defined for the full list of differences from
275+
classic `<link rel="stylesheet">` elements? This is tracked in the
276+
[planning document](https://docs.google.com/document/d/1SkHwxAIBW5I3uqnmmov4D71ZPbj9woouj3RdPqd3X1w).
277+
2. Should we attempt to make this feature backward-compatible with
278+
classic `<link rel="stylesheet">` elements, or are there too many
279+
differences?
280+
3. Is `href` the right way to go, even though it goes through import map
281+
processing? Should we use a new attribute such as `moduleimport`?
282+
4. How should the cardinality issues for attributes like `media` be
283+
addressed?
284+
285+
## References & Acknowledgements
286+
287+
Many thanks for valuable feedback and advice from:
288+
289+
- Alison Maher
290+
- Dan Clark
291+
- Emilio Cobos Álvarez
292+
- Hoch Hochkeppel
293+
- Jake Archibald
294+
- Justin Fagnani
295+
- Keith Cirkel
296+
- Lea Verou
297+
- Mason Freed
298+
- Noam Rosenthal
299+
- Steve Orvell

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ we move them into the [Alumni section](#alumni-) below.
8585
| [Handwriting attribute](Handwriting/explainer.md) | <a href="https://github.com/MicrosoftEdge/MSEdgeExplainers/labels/Handwriting"> ![GitHub issues by-label](https://img.shields.io/github/issues/MicrosoftEdge/MSEdgeExplainers/Handwriting?label=issues)</a> | [New issue...](https://github.com/MicrosoftEdge/MSEdgeExplainers/issues/new?assignees=adettenb&labels=Handwriting&title=%5BHandwriting%5D+Issue) | HTML |
8686
| [Mulitple Stylesheets Per File (@sheet)](AtSheet/explainer.md) | <a href="https://github.com/MicrosoftEdge/MSEdgeExplainers/labels/AtSheet">![GitHub issues by-label](https://img.shields.io/github/issues/MicrosoftEdge/MSEdgeExplainers/AtSheet?label=issues)</a> | [New Issue...](https://github.com/MicrosoftEdge/MSEdgeExplainers/issues/new?assignees=aluhrs13&labels=AtSheet&title=%5B%40sheet%5D+%3CTITLE+HERE%3E) | CSS |
8787
| [Link Rel Local Reference](LocalReferenceLinkRel/explainer.md) | <a href="https://github.com/MicrosoftEdge/MSEdgeExplainers/labels/LRLR">![GitHub issues by-label](https://img.shields.io/github/issues/MicrosoftEdge/MSEdgeExplainers/LocalReferenceLinkRel?label=issues)</a> | [New Issue...](https://github.com/MicrosoftEdge/MSEdgeExplainers/issues/new?assignees=KurtCattiSchmidt&labels=LRLR&title=%5BLRLR%5D+%3CTITLE+HERE%3E) | HTML |
88+
| [Style Module Imports via `<link>` Elements](LinkTagModuleImport/explainer.md) | <a href="https://github.com/MicrosoftEdge/MSEdgeExplainers/labels/LinkTagImport">![GitHub issues by-label](https://img.shields.io/github/issues/MicrosoftEdge/MSEdgeExplainers/LinkTagImport?label=issues)</a> | [New Issue...](https://github.com/MicrosoftEdge/MSEdgeExplainers/issues/new?assignees=KurtCattiSchmidt&labels=LinkTagImport&title=%5BLinkTagImport%5D+%3CTITLE+HERE%3E) | HTML |
8889
| [HighlightsFromPoint](highlight/HighlightsFromPointsExplainer.md)| <a href="https://github.com/MicrosoftEdge/MSEdgeExplainers/labels/HighlightEvents">![GitHub issues by-label](https://img.shields.io/github/issues/MicrosoftEdge/MSEdgeExplainers/HighlightEvents?label=issues)</a>| [New issue...](https://github.com/MicrosoftEdge/MSEdgeExplainers/issues/new?template=highlight-events.md)| CSS |
8990
| [Digital Goods](PwaDigitalGoods/explainer.md)| <a href="https://github.com/MicrosoftEdge/MSEdgeExplainers/labels/Digital%20Goods%20API">![GitHub issues by-label](https://img.shields.io/github/issues/MicrosoftEdge/MSEdgeExplainers/Digital%20Goods%20API?label=issues)</a>| [New issue...](https://github.com/MicrosoftEdge/MSEdgeExplainers/issues/new?template=digital-goods.md)| Web Applications |
9091
| [Search Providers Reset](SearchProviders/explainer.md)| <a href="https://github.com/MicrosoftEdge/MSEdgeExplainers/labels/Search%20Providers%20Reset">![GitHub issues by-label](https://img.shields.io/github/issues/MicrosoftEdge/MSEdgeExplainers/Search%20Providers%20Reset?label=issues)</a>| [New issue...](https://github.com/MicrosoftEdge/MSEdgeExplainers/issues/new?template=search-providers-reset.md)| WICG |

0 commit comments

Comments
 (0)