|
| 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 |
0 commit comments