|
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 |
| 1 | +This explainer has been integrated into [Declarative Shadow DOM Style Sharing](../ShadowDOM/explainer.md). |
0 commit comments