MSEdgeExplainers

Declarative adoptedStyleSheets for Sharing Styles In Declarative Shadow DOM

Authors

Participate

Status of this Document

This document is intended as a starting point for engaging the community and standards bodies in developing collaborative solutions fit for standardization. As the solutions to problems described in this document progress along the standards-track, we will retain this document as an archive and use this section to keep the community up-to-date with the most current standards venue and content location of future work and discussions.

Table of Contents

Background

With the use of web components in web development, web authors often encounter challenges in managing styles, such as distributing global styles into shadow roots and sharing styles across different shadow roots. Markup-based shadow DOM, or Declarative Shadow DOM (DSD), is a new concept that makes it easier and more efficient to create a shadow DOM definition directly in HTML, without needing JavaScript for setup. Shadow DOM provides isolation for CSS, JavaScript, and HTML. Each shadow root has its own separate scope, which means styles defined inside one shadow root do not affect another or the main document.

Declarative Shadow DOM (DSD) is a markup-based (declarative) alternative to script-based (imperative) Shadow DOM. Imperative Shadow DOM currently supports the adoptedStyleSheets property, which allows for sharing stylesheets between shadow roots, but Declarative Shadow DOM does not have a declarative solution for sharing inline styles. This proposal aims to address this gap with the introduction of <style type="module">, which defines inline style modules to share, and the shadowrootadoptedstylesheets attribute on the <template> tag as an analog to Imperative Shadow DOM's adoptedStyleSheets property.

Problem

Sites that make use of Declarative Shadow DOM (DSD) have reported that the lack of a way to reference repeated stylesheets creates large payloads that add large amounts of latency and increased memory overhead. Authors have repeatedly asked for a way to reference stylesheets from other DSD instances in the same way that frameworks leverage internal data structures to share constructable style sheets via adoptedStyleSheets. This Explainer explores several potential solutions.

Relying on JavaScript for declaratively styling shadow roots via the imperative adoptedStyleSheets property is not ideal for several reasons:

While referencing an external file via the tag for shared styles in DSD works today (and is currently recommended by DSD implementors), it is not ideal for several reasons:

This example shows how a developer might use DSD to initialize a shadow root without JavaScript.

  <article-card>
    <template shadowrootmode="open">
       <style>
         :host {
            border: 1px solid #e0e0e0;
          }
       </style>
    </template>
  </article-card>

While this approach is acceptable for a single component, a rich web application may define many <template> elements. Since pages often use a consistent set of visual styles, these <template> instances must each include <style> tags with duplicated CSS, leading to unnecessary CPU costs and memory overhead.

This document explores several proposals that would allow developers to apply styles to DSD without relying on JavaScript and avoiding duplication.

Goals

Non-goals

Some developers have expressed interest in CSS selectors crossing through the Shadow DOM, as discussed in issue 909. While this scenario is related to sharing styles with Shadow DOM elements, it is solving a different problem and should be addressed separately.

Use case

Anywhere web components are used

When asked about pain points in Web Components, the number one issue, with 13% of the vote, is styling and customization. Many respondents specifically mentioned the difficulty of style sharing issues within a shadow DOM:

For additional use cases, please see issue 939.

Streaming SSR

With Server-Side-Rendering (SSR), servers emit HTML markup to the client's web browser. When this markup is emitted as a stream, the full document's DOM structure may not have been determined ahead of time. Standard DOM scoping behaves such that Shadow DOM nodes can only access identifiers in their own shadow root and in the light DOM. This situation makes it impossible to share styles between shadow roots, leading to duplication of style rules and markup. This duplication is especially painful for SSR scenarios, which are typically heavily optimized for performance.

The proposed global scope for declarative CSS Modules is essential to this scenario because it allows nested shadow roots to share a global set of styles. Standard DOM scoping rules would not work here, as demonstrated by the following example:

<template shadowrootmode="open" shadowrootadoptedstylesheets="my-component-styles">
  <!-- Emit styles that might need to be shared later. -->
  <style type="module" specifier="my-component-styles">...</style>
  <div>...component content...</div>
  <!-- A child component is emitted that needs the same set of shared styles. Since the shared styles were already emitted above, they can be re-used with `shadowrootadoptedstylesheets`. -->
  <template shadowrootmode="open" shadowrootadoptedstylesheets="my-component-styles">
    <!-- Styles are shared from the parent shadow root (this would not work with standard DOM scoping, which can only access identifiers in this shadow root and the light DOM). -->
    <div>...component content...</div>
  </template>
</template>
<!-- Sibling component with shared styles. Again, since shared styles were already emitted, they can be re-used via `shadowrootadoptedstylesheets`. -->
<template shadowrootmode="open" shadowrootadoptedstylesheets="my-component-styles">
  <!-- Styles are shared from the sibling shadow root (this would also not work with standard DOM scoping). -->
  <div>...component content...</div>
</template>

Alternatives to using style in DSD

Constructable Stylesheets

Developers can create stylesheets that can be applied to multiple shadow roots, using existing JavaScript, as outlined by the example below.

Step 1: Create a new Constructable Stylesheet:

const constructableStylesheet = new CSSStyleSheet();

Step 2: Add styles to the Constructable Stylesheet:

constructableStylesheet.replaceSync(`
  .my-button {
    background-color: #0074D9;
  }
`);

Step 3: Attach the Constructable Stylesheet to the shadow root:

shadow.adoptedStyleSheets = [constructableStylesheet];

A downside of this approach is a potential FOUC, where the element is initially painted without styles, and then repainted with the Constructable Stylesheet. Another downside to this approach is that it requires script, which might be disabled. Even if enabled, requiring script to apply styles somewhat defeats the purpose of Declarative Shadow DOM (DSD).

Using rel="stylesheet" attribute

Using <link rel="stylesheet"> to share styles across Shadow DOM boundaries helps maintain consistent design, avoids extraneous parsing that duplicated <style> tags would necessitate, and reduces component sizes for faster load times. However, it can cause redundant network requests since each component that uses <link rel="stylesheet"> within its Shadow DOM may trigger an expensive operation such as a network request or a disk access. Also note that <link rel="stylesheet"> is not render blocking when it's in the <body> (as Declarative Shadow DOM nodes typically are), which can cause a FOUC.

CSS @import rules

Global styles can be included in a single stylesheet, which is then importable into each shadow root to avoid redundancy. The downsides are the exact same as in Using rel="stylesheet" attribute, with an additional disadvantage that multiple @import statements are loaded sequentially (while <link> tags will load them in parallel).

Proposal: Inline, declarative CSS module scripts

This proposal builds on CSS module scripts, enabling authors to declare a CSS module inline in an HTML file and link it to a DSD using its module specifier. A type=”module” attribute on the <style> element would define it as a CSS module script and the specifier attribute would add it to the module cache as if it had been imported. This allows the page to render with the necessary CSS modules attached to the correct scopes without needing to load them multiple times. Note that module maps are global, meaning that modules defined in a Shadow DOM will be accessible throughout the document context.

<style type="module" specifier="foo">
  #content {
    color: red;
  }
</style>

Given this <style> tag, the styles could be applied to a DSD as follows:

<my-element>
  <template shadowrootmode="open" shadowrootadoptedstylesheets="foo">
    ...
  </template>
</my-element>

The shadow root will be created with its adoptedStyleSheets array containing the "foo" CSS module script. This single CSS module script can be shared by any number of shadow roots.

An inline CSS module script could also be imported in a JavaScript module in the usual way:

import styles from 'foo' with { type: 'css' };

Another advantage of this proposal is that it can allow multiple module specifiers in the shadowrootadoptedstylesheets property:

<style type="module" specifier="foo">
  #content {
    color: red;
  }
</style>

<style type="module" specifier="bar">
  #content {
    font-family: sans-serif;
  }
</style>

<my-element>
  <template shadowrootmode="open" shadowrootadoptedstylesheets="foo bar">
    ...
  </template>
</my-element>

Due to the global nature of specifier in this context, it could be called exportspecifier, to emphasize the fact that it has effects outside the shadow root.

Scoping

The module map exists today as a global registry per document, not scoped to a particular shadow root. Many developers have expressed interest in such a global map for sharing stylesheets, as it allows for nested shadow roots to access a base set of shared styles without needing to redefine them at each level of shadow root nesting.

A global map does come with some tradeoffs, particularly when names collide. With a global map, nested shadow roots could override entries from parent shadow roots, which could be undesirable.

<script> vs <style> For CSS Modules

Earlier versions of this document used the <script> tag for declaring CSS Modules, which would be more consistent with the current set of module types (as they are all script-related). Developer feedback has shown a strong preference for using the <style> tag when declaring CSS Modules, so this proposal has been updated accordingly. This concept of using a non-<script> tag for defining Declarative CSS Modules could be expanded for future declarative modules such as HTML and SVG. The <script> tag remains a natural wrapper for other declarative modules that are script-based, such as JavaScript, JSON, and WASM.

Behavior with script disabled

User agents allow for disabling JavaScript, and declarative modules should still work with JavaScript disabled. However, the module graph as it exists today only functions with script enabled. Browser engines should confirm whether this is feasible with their current implementations. Chromium has been verified as compatible, but other engines such as WebKit and Gecko have not been verified yet.

Syntactic Sugar For Import Maps with Blob URL

The simplest approach for Declarative CSS Modules is to treat them as syntactic sugar that generates an Import Map entry containing a specifier and a Blob URL referencing a Blob containing the module contents.

For example, a Declarative CSS Module defined as follows:

<style type="module" specifier="foo">
  #content { color: red; }
</style>

...would be syntactic sugar for:

<script>
  const blob_url = URL.createObjectURL(new Blob(["#content { color: red; }"], {type: "text/css"}));
</script>
<script type="importmap">
{
  "imports": {
    "foo": "<value of `blob_url`>"
  }
}
</script>

...and importing the module declaratively like this:

<template shadowrootmode="open" shadowrootadoptedstylesheets="foo">...</template>

...could be syntactic sugar for:

<script type="module">
const shadowRoot = ...;
import("foo", {with: { type: "css" }}).then(foo=>shadowRoot.adoptedStyleSheets.push(foo));
</script>

This approach is much simpler than alternate proposals and avoids nearly all of the issues associated with them because it builds on existing concepts.

This approach does have a few limitations:

Blob URLs are active for the lifetime of the page on which they were created and are revoked via revokeObjectURL. A developer could theoretically discover the URL generated from a Declarative CSS Module and revoke it, but this doesn't expose any new issues as this scenario is already possible to do imperatively.

There are several options for managing the lifetime of the generated Blob object. For instance, it could be revoked when the <style type="module"> that created it is disconnected. This would give developers some options for managing Blob lifetimes, but once revoked, Blob URLs cannot be reused, so re-inserting the <style type="module"> tag cannot undo it being removed. Generating a new Blob URL and adding it to the Import Map will not work either, since Import Maps will ignore subsequent entries with an existing specifier. By default, Blob URLs generated with Declarative CSS Modules would be tied to the lifetime of the document, with no options for revoking them. This would result in consistent behaviors for developers, at the expense of flexibility with resource management. Not exposing the ability to revoke the Blob URL aligns with how Import Maps behave, so it is the preferred option.

Alternatively, a data URI could be used instead of a Blob URL. However, using a Blob URL offers several performance advantages over a data URI, such as avoiding URL-encoding and a much smaller Import Map value string stored in memory.

Using Data URI's, a Declarative CSS Module defined as follows:

<style type="module" specifier="foo">
  #content { color: red; }
</style>

...would be syntactic sugar for:

<script type="importmap">
{
  "imports": {
    "foo": "data:text/css,%23content { color: red; }"
  }
}
</script>

The data URI must be URL-encoded, because many CSS selectors have special meaning in URLs. One example is the # ID selector in CSS, which is a fragment identifier in URLs and can only exist once in a URL. Importing via shadowrootadoptedstylesheets would work exactly the same as the Blob URL example above.

Detailed Parsing Workflow

In the following example:

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

Upon parsing the <style> tag above, an import map string is generated with JSON containing a map with a key of "imports". The value associated with this key is another JSON map with a single entry with a key containing the value of the specifier attribute on the <style> tag (in this case, "foo"). The value associated with this key is a Blob URI with a media type of "text/css" and a value of the text content of the <style> tag. Alternatively, the value associated with the key is a data URI with a scheme of "data", a media type of "text/css", and data consisting of a UTF-8 percent encoded value of the text content of the <style> tag.

Note that unlike a regular <style> tag with CSS content, the sheet attribute defined in the LinkStyle interface would always be empty for Declarative CSS Modules. Similarly, updating the text content of the <style> tag would not update the generated import map string, which is exactly how import maps behave when their text content is modified.

This generated import map string is then processed using the same 'parse an import map string' algorithm as a typical import map.

When the <template> element is constructed, the shadowrootadoptedstylesheets attribute is evaluated. Each space-separated identifier in the attribute performs an import of that specifier with a module type of "css". If the result of that import is successful, the associated CSS module script's default export of type CSSStyleSheet is added to the adoptedStyleSheets backing list associated with the <template> element's shadow root in specified order, as defined in CSS Style Sheet Collections. This would allow for importing both Declarative CSS Modules and previously-fetched imperative CSS Modules via the shadowrootadoptedstylesheets attribute.

As with existing <style> tags, if the CSS contains invalid syntax, error handling follows the rules specified in error handling.

Styles would not be applied retroactively, as in the following example:

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

When the <template> element is parsed, an import of "foo" with a module type of "css" is performed. This import is unsuccessful, as the module map does not contain an entry with a specifier of "foo".

When the <style> element's specifier attribute is parsed, an import map string is generated with JSON containing the contents as a data URI as specified above. Since the adoptedStyleSheets backing list associated with the <template> element's shadow root was not populated, no styles are applied to the shadow root.

Only the first instance of a given specifier is added to the module map, because the merge module specifier maps algorithm enforces that only the first instance of a given specifier mapping is applied, and subsequent duplicate specifier mappings are ignored.

For example, with the following markup:

<style type="module" specifier="foo">
  #content {
    color: red;
  }
</style>
<style type="module" specifier="foo">
  #content {
    color: blue;
  }
</style>
<my-element>
  <template shadowrootmode="open" shadowrootadoptedstylesheets="foo">
    ...
  </template>
</my-element>

The contents of the first Declarative CSS Module with specifier="foo" (with color: red) are first parsed and the import map is created as specified above.

Upon parsing the second Declarative CSS Module with specifier="foo" (with color: blue), an import map is created as specified above. Per the merge module specifier maps algorithm, only the first instance of a given specifier mapping is applied, and subsequent duplicate specifier mappings are ignored.

The <template> with shadowrootadoptedstylesheets="foo" will use the first definition (with color: red).

This scenario may also occur when the <style> element is a child of the <template> that adopts it, as shown in the following example:

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

In this example, the <template> element is parsed first. When the <template> element is parsed, an import of "foo" with a module type of "css" is performed. This import is unsuccessful, as the module map does not contain an entry with a specifier of "foo".

The contents of the Declarative CSS Module with specifier="foo" (with color: red) are then parsed and an import map is created as specified above. Since the <template> element failed to import a module, the color: red styles will not be applied, although subsequent <template> elements could adopt a stylesheet with specifier="foo" now that it has been defined.

Use with External CSS Files

The <template> element's shadowrootadoptedstylesheets attribute does not differentiate between specifiers created declaratively (via <style type="module">) or external CSS files. This means that the following example is valid:

<my-element>
  <template shadowrootmode="open" shadowrootadoptedstylesheets="./foo.css">
    ...
  </template>
</my-element>

...where "foo.css" is an external CSS file. Note that shadowrootadoptedstylesheets only queries the module map - it doesn't perform a fetch. Developers must instead pre-fetch the CSS file and add it to the module map before the <template> tag is parsed. This can be done imperatively with a JavaScript import statement within a <script type="module">, but requiring script for this scenario is not ideal.

This can be handled declaratively with the existing <link rel="modulepreload">, which fetches a module and adds it to the module map.

However, <link rel="modulepreload"> does not currently work with CSS Module Scripts. This has been proposed by the WHATWG in Issue 10233 and makes sense to prioritize to allow external CSS files to work declaratively with shadowrootadoptedstylesheets.

This alone does not make shadowrootadoptedstylesheets work well with external files, as <link rel="modulepreload"> does not perform a synchronous fetch, and if the fetch has not completed by the time the shadowrootadoptedstylesheets attribute is parsed, the styles will not be available in the module map.

This scenario could be handled by supporting the blocking attribute on <link rel="modulepreload">, which should be considered for this feature.

All together, a fully-functional example of using shadowrootadoptedstylesheets with an external CSS file would look like this:

<my-element>
  <link rel="modulepreload" as="style" href="./foo.css" blocking="render">
  <template shadowrootmode="open" shadowrootadoptedstylesheets="./foo.css">
    ...
  </template>
  <template shadowrootmode="open" shadowrootadoptedstylesheets="./foo.css">
    ...
  </template>
</my-element>

Note that the second <template> tag doesn't need a corresponding <link rel="modulepreload"> - this only needs to happen once per external module, per document, to ensure that it's in the module map before shadowrootadoptedstylesheets is parsed.

Importing Other CSS Files With @import

Imperative CSS Module Scripts cannot import other CSS Module Scripts. The existence of a CSS @import statement within the text content of an Imperative CSS Module Script results in a script error when imported. Many possible solutions for importing child CSS modules have been discussed in https://github.com/WICG/webcomponents/issues/870, but there is no agreed upon general solution.

Given this existing limitation with @import for Imperative CSS Module Scripts, we do not believe that this is a blocking issue for Declarative CSS Module Scripts. That said, Declarative CSS Module Scripts provide a new method for creating CSS Modules, which introduces another opportunity for addressing this limitation. This will be investigated as a separate proposal that can be addressed in parallel to this proposal.

Declarative CSS Modules cannot throw script errors when encountering an @import statement because script errors can only be thrown in a scripting environment. A reasonable alternative for Declarative CSS Modules is to fail parsing for the module when an @import is parsed and log an error in developer tools until a solution for importing nested CSS Modules has been implemented.

Use with Imperative Module Scripts

Declarative CSS Modules can be used with imperative module scripts from within a static import.

Consider the following example:

<style type="module" specifier="foo">
  ...
</style>

Script can later insert this module into an adoptedStyleSheets array as follows:

import sheet from "foo" with { type: "css" };
shadowRoot.adoptedStyleSheets = [sheet];

...assuming that "foo" hasn't been used as the key of an import map that redirects it to a URL. If "foo" has used as a key of an import map that redirects to a URL, that URL will be fetched instead of locating the declarative version.

If a module is imported imperatively in this fashion and the Declarative CSS Module is not in the module map, the import fails, even if it is added declaratively at a later time.

Static Versus Dynamic Values For shadowrootadoptedstylesheets Specifiers

Stylesheets referenced by specifiers in the shadowrootadoptedstylesheets attribute are static - they are only applied if they are available when their associated <template> tag is parsed.

In the following example, the stylesheet referenced by the specifier "foo" is not applied to the adopted stylesheets array because the declarative definition of "foo" is after the <template> tag:

<my-element>
  <template shadowrootmode="open" shadowrootadoptedstylesheets="foo">
    <div id="content">filler text</div>
  </template>
</my-element>
<style type="module" specifier="foo">
  #content {
    color: red;
  }
</style>

Similarly, for external files, no styles are adopted into the <template> because "foo.css" hasn't been loaded into the module map at the time the <template> tag has been parsed:

<my-element>
  <template shadowrootmode="open" shadowrootadoptedstylesheets="./foo.css">
    ...
  </template>
</my-element>
<link rel="modulepreload" as="style" href="./foo.css">

This could be addressed by treating the specifier as a dynamic reference and invalidating styles accordingly when that reference changes. However, supporting this scenario is not ideal for several reasons:

For these reasons, we do not believe it is worth pursuing making specifiers dynamic.

Why shadowrootadoptedstylesheets Does Not Perform a Fetch

The current design does not fetch the specifiers listed in shadowrootadoptedstylesheets - they must be present in the module map at the time shadowrootadoptedstylesheets is parsed.

There are several reasons for this behavior:

For these reasons, we do not believe that it is necessary to perform a fetch for each entry in shadowrootadoptedstylesheets.

Other Declarative Modules

An advantage of this approach is that it can be extended to solve similar issues with other content types. Consider the case of a declarative component with many instances stamped out on the page. In the same way that the CSS must either be duplicated in the markup of each component instance or set up using script, the same problem applies to the HTML content of each component. We can envision an inline version of HTML module scripts that would be declared once and applied to any number of shadow root instances:

<template type="module" specifier="foo">
<!-- This template defines an HTML module whose contents are given by the markup
     placed here, inserted into the module map with the specifier "foo" -->
...
</template>
<my-element>
<!-- The `shadoowroothtml` attribute causes the `<template>` to populate the shadow root by
cloning the contents of the HTML module given by the "foo" specifier, instead of
parsing HTML inside the <template>. -->
  <template shadowrootmode="open" shadowroothtml="foo"></template>
</my-element>

In this example we’ve leveraged the module system to implement declarative template refs.

This approach could also be expanded to SVG modules, similar to the HTML Modules example above.

<template type="module" specifier="foo">
<!-- This template defines an SVG module whose contents are given by the SVG markup
     placed here, inserted into the module map with the specifier "foo" -->
...
</template>
<my-element>
<!-- The `shadoowroothtml` attribute causes the `<template>` to populate the shadow root by
cloning the contents of the SVG module given by the "foo" specifier, instead of
parsing SVG inside the <template>. -->
  <template shadowrootmode="open" shadowroothtml="foo"></template>
</my-element>

SVG makes heavy use of IDREF's, for example href on <use> and SVG filters. Per existing Shadow DOM behavior, these IDREF's would be scoped per shadow root.

CSS Modules are not the only type of module - there are also JavaScript, JSON, SVG, HTML, and WASM that need to be considered.

Module type Script Module Declarative Module
JavaScript import { foo } from "./bar.js"; <script type="module" specifier="bar"></script>
CSS import foo from "./bar.css" with { type: "css" }; <style type="module" specifier="bar"></style>
JSON import foo from "./bar.json" with { type: "json" }; <script type="json-module" specifier="bar"></script>
HTML import {foo} from "bar.html" with {type: "html"}; <template type="html-module" specifier="bar"></template>
SVG import {foo} from "bar.svg" with {type: "svg"}; <template type="svg-module" specifier="bar"></template>
WASM import {foo} from "bar.wasm" with {type: "wasm"}; <script type="wasm-module" specifier="bar"></script>

Modules that support declarative content (such as CSS Modules and HTML Modules) need both a declarative export mechanism (<style type="module"> for CSS Modules) and a declarative import mechanism (the adoptedstylesheets attribute and/or the <link> tag for CSS Modules), while purely script-based modules types (such as JavaScript, JSON, and WASM) only require a declarative export mechanism, as they are expected to be imported via script.

The following example demonstrates how a JavaScript module could be exported declaratively and imported imperatively:

<script type="module" specifier="foo">
  export const magic_number = 42;
</script>
<script type="module">
  import {magic_number} from "foo";
  console.log(magic_number);
</script>

...and likewise for a JSON module:

<script type="json-module" specifier="foo">
{"people": [{"craft": "ISS", "name": "Oleg Kononenko"}, {"craft": "ISS", "name": "Nikolai Chub"}], "number": 2, "message": "success"}
</script>
<script type="module">
  import people_in_space from "foo" with { type: "json" };
  console.log(people_in_space.message);
</script>

Alternate proposals

Updates to Module Map Key

An alternative proposal involves modifying the module map to be keyed by a string instead of a URL (the current key is a (URL, module type) pair, which would be changed to a (string, module type) pair). A string is a superset of a URL, so this modification would not break existing scenarios.

This requirement could be avoided by instead requiring a declarative specifier to be a URL fragment, but we believe this would introduce several potentially confusing and undesirable outcomes:

  1. The Find a potential indicated element algorithm only searches the top-level document and does not query shadow roots. While this proposal does not require the find a potential indicated element to function (the indicated element in this case is the <style> element that is directly modifying the module map, so there is no element to find), it could be confusing to introduce a new fragment syntax intended for use in shadow roots that violates this principle.
  2. Import maps remap URL's, which allows relative and bare URL's to map to a full URL. It's not clear if there is a use case for remapping same-document references with import maps that cannot be accomplished by adjusting the local reference's identifier. If import maps are performed on a same-document URL reference, an import map entry intended for an external URL could unintentionally break a local reference. Import map resolution could be adjusted to skip same-document references, but it could be confusing to have a URL identifier that does not participate in the resolved module set.
  3. HTML documents are already using fragments for many different concepts, such as fragment navigations, history updates, internal resource links, SVG href targets, and more. Although these use cases are very different, a common factor between them is that they all reference elements in the main document, and cannot refer to elements within a shadow root. An important piece of this proposal is that nested shadow roots can modify the global module map. Introducing a new scoping behavior for fragments that does not fit this model could be confusing to authors.
  4. URL's that consist only of a fragment resolve to a relative URL, with the base url defined as the source document per the URL parsing algorithm. This means that using a fragment-only syntax (which would be desired in this scenario) could break if a <base> element exists that remaps the document's base URL.

Another alternative could be to define a new scheme for local references. This is a potential solution, however, since the containing HTML document already has a scheme, this option would require developers to always specify the scheme per absolute URL with fragment string processing, rather than just the fragment (a fragment-only URL is valid due to the way relative URL processing applies). Developers might find it cumbersome to specify the scheme for local references versus an approach that requires only an identifier (for example, localid://foo versus #foo or foo). A new scheme could also imply scoping behaviors that are not supported, such as external-file references that are valid in SVG, or potentially even imply that module identifiers can span between <iframe> documents. A new scheme may also not be compatible with existing custom scheme handlers.

The samples listed use a proposed shadowrootadoptedstylesheets attribute on the <template> tag with a space-separated list of specifiers. This closely maps to the existing JavaScript adoptedStyleSheets property.

Another option is to instead use existing HTML concepts for applying stylesheets into shadow roots, such as the <link> tag, as demonstrated by the following example:

<style type="module" specifier="foo">
  #content {
    color: red;
  }
</style>

<my-element>
  <template shadowrootmode="open">
    <link rel="adoptedstylesheet" specifier="foo">
  </template>
</my-element>

While this approach doesn't map as closely to the existing adoptedStyleSheets API, it more closely follows existing HTML semantics. It also allows for a rich set of features offered by the <link> element, such as media queries. However, a small disadvantage is that the <link> element has many additional properties that would not apply in this scenario, such as crossorigin, fetchpriority, referrerpolicy.

The shadowrootadoptedstylesheets attribute as specified accepts a list a stylesheets. Multiple stylesheets can be added to a shadow root's adopted stylesheet list with the <link> proposal by including multiple <link> tags.

Looking forward, the <link> approach is directly compatible with the proposed CSS @sheet feature, which allows a single CSS file to contain multiple stylesheets. This allows developers to specify a single named stylesheet that is applied from the CSS definition, rather than applying the global contents of the entire sheet, as illustrated by the following example:

<style type="module" specifier="foo">
  @sheet my_cool_sheet {
    ...
  }
  @sheet my_other_sheet {
    #content {
    ...
    }
  }
  #content {
    ...
  }
</style>

<my-element>
  <template shadowrootmode="open">
    <link rel="adoptedstylesheet" specifier="foo" sheet="my_cool_sheet">
  </template>
</my-element>

Only the contents of my_cool_sheet would be applied, due to the sheet attribute on the <link> tag specifying that named sheet.

This proposal extends the existing <link> tag to support local <style> tag references as follows:

<style id="inline_styles">
  p {
    color: blue;
  }
</style>
<p>Outside Shadow DOM</p>
<template shadowrootmode="open">
  <link rel="stylesheet" href="#inline_styles" />
  <p>Inside Shadow DOM</p>
</template>

This allows for sharing styles defined in the Light DOM across Shadow Roots. Due to scoping behaviors, it will not allow for styles defined in a Shadow DOM to be accessed in any other Shadow Root. This limitation could be addressed with extensions on Shadow DOM scoping suggested in this thread.

Both this proposal and Local References For Link Rel allow authors to share inline CSS with Shadow Roots. There are some key differences in both syntax and behaviors, as illustrated in the following table:

Local Reference Link Rel Declarative CSS Modules
Scope ⚠️ Standard DOM scoping Global scope
Identifier syntax Standard HTML IDREF Module identifier
Attribute used Standard HTML href New attribute for identifier
Uses existing HTML concepts ✅ Yes ❌ No
Uses existing module concepts ❌ No ✅ Yes
Extensibility Clean @sheet integration, scope expansion could apply to SVG references More declarative module types (HTML, SVG, etc.)

Layer and adoptStyles

This proposal adds the adoptStyles attribute to the template element, enabling its shadow root to adopt styles from outside of the shadow DOM.

Here is an example that shows how the proposed adoptStyles is used declaratively:

<!-- Define styles in the outer context -->
<style>
  @layer base {
    body {
      font-family: Arial, sans-serif;
    }
  }

  @layer theme {
    .button {
      color: white;
      background-color: blue;
    }
  }

</style>

<!-- Define a custom element that adopts styles from the outer context page style -->
<custom-element >
  <template shadowroot="open" adoptstyles="inherit.theme, inherit.base">
    <style>
      ...
    </style>
    <button class="button shadow-button">Click Me</button>
  </template>
</custom-element>

In this example, the adoptstyles attribute on the <template> specifies that the shadow DOM should inherit styles from two outer context layers, using a list of style references, inherit.theme and inherit.base.

A similar adoptstyles JavaScript API can set and return a styleReferenceList, which is a list of style references associated with the shadow root. This list can be set and retrieved, with specific formats for inheriting, renaming, or reverting styles.

The method aims to support both declarative and imperative shadow trees and work seamlessly with existing CSS features like @layer and @scope. However, there may be a FOUC issue with loading external stylesheets.

Since CSS is scoped per Shadow Root, nested Shadow DOM elements would need to inherit at each level.

@Sheet

This proposal builds on using multiple sheets per file that introduces a new @sheet rule to address the difficulties arising when using JavaScript modules to manage styles. The main idea is to enhance the way CSS is imported, managed, and bundled in JavaScript by allowing multiple named stylesheets to exist within a single CSS file. We can expand on this proposal to allow stylesheets being directly specified within the HTML markup using shadowrootadoptedstylesheets property without requiring JavaScript:

<style>
  @sheet sheet1 { *: background-color: gray; }
  @sheet sheet2 { *: color: blue; }
</style>

<template shadowrootmode="open" shadowrootadoptedstylesheets="sheet1 sheet2">
  <span>I'm in the shadow DOM</span>
</template>

In this example, developers could define styles in a <style> block using an @sheet rule to create named style sheets. The adoptedStyleSheets property allows Shadow DOMs to specify which stylesheets they want to adopt without impacting the main document, improving ergonomics.

The JavaScript version of this could also support CSS modules:

@sheet sheet1 {
  :host {
    display: block;
    background: red;
  }
}

@sheet sheet2 {
  p {
    color: blue;
  }
}
<script>
import {sheet1, sheet2} from './styles1and2.css' assert {type: 'css'};
...
shadow.adoptedStyleSheets = [sheet1, sheet2];
</script>

This approach could be combined with other approaches listed in this document.

The specification of @sheet could be modified to split the definition of stylesheets from the application of the style rules. With this modification, @sheet would define a stylesheet with its own set of rules, but not apply the rules automatically. This would allow for defining stylesheets in a light DOM context and applying them only to the shadow roots.

With this behavior, the following example would have a gray background and blue text only within the Shadow DOM:

<style>
  @sheet sheet1 { *: background-color: gray; }
  @sheet sheet2 { *: color: blue; }
</style>
<span>I am in the light DOM</span>
<template shadowrootmode="open" shadowrootadoptedstylesheets="sheet1 sheet2">
  <span>I'm in the shadow DOM</span>
</template>

The light DOM could opt into particular stylesheets defined by @sheet via existing mechanisms such as @import:

<style>
  @sheet sheet1 { *: background-color: gray; }
  @sheet sheet2 { *: color: blue; }
  @import sheet("sheet1");
  @import sheet("sheet2");
</style>

A similar mechanism for @sheet was proposed in this comment.

Stylesheets defined via @sheet are not global - they are scoped per shadow root. Nested shadow roots may share stylesheets between shadow roots by passing down the identifier at each layer via shadowrootadoptedstylesheets and using @import to apply the stylesheet, as illustrated in the following example:

<style>
  @sheet sheet1 { *: color: blue; }
</style>
<span>I am in the light DOM</span>
<template shadowrootmode="open" shadowrootadoptedstylesheets="sheet1">
  <style>
    @import sheet("sheet1");
  </style>
  <span>I'm in the first layer of the shadow DOM and my text should be blue</span>
  <template shadowrootmode="open" shadowrootadoptedstylesheets="sheet1">
    <style>
      @import sheet("sheet1");
    </style>
    <span>I'm in the second layer of the shadow DOM and my text should be blue</span>
    <template shadowrootmode="open">
      <span>I'm in the third layer of the shadow DOM and my text should not be blue because this layer doesn't have `shadowrootadoptedstylesheets`</span>
    </template>
  </template>
  <template shadowrootmode="open" shadowrootadoptedstylesheets="sheet1">
    <span>I'm also in the second layer of the shadow DOM and my text should not be blue because I didn't `@import` the adopted stylesheet, even though I specified it via `shadowrootadoptedstylesheets`</span>
  </template>
</template>

Text within both shadow roots in the above example should be blue due to the shadowrootadoptedstylesheets at each Shadow DOM layer. Note that it is not currently possible to export stylesheets out of shadow roots, which is a deal-breaker for the Streaming SSR example outlined above.

An alternative to this entire proposal would be to make @sheet identifiers cross shadow boundaries, which would also allow for sharing styles across shadow roots. However, without a way to import inline <style> blocks into shadow roots, as proposed in Local References in Link Tags, this behavior would be limited to external .css files. Due to DOM scoping, Local References in Link Tags would not work as required in a Streaming SSR scenario.

Id-based shadowrootadoptedstylesheets attribute on template

This proposal will add a new markup-based shadowrootadoptedstylesheets property that closely matches the existing JavaScript property. The behavior would be just like the adoptedStyleSheet property that already exists in JavaScript, except it would accept a list of id attributes instead of a ConstructableStylesheet JavaScript object.

<style type="css" id="shared_shadow_styles">
    :host {
      color: red
    }
</style>

or

<link rel=”stylesheet” href=”styles.css” id=”external_shared_shadow_styles”>

Web authors can use the shadowrootadoptedstylesheets property on the <template> element to associate the stylesheets with a declarative shadow root.

<template shadowrootmode="open" shadowrootadoptedstylesheets="shared_shadow_styles external_shared_shadow_styles">
      <!-- -->
</template>

One requirement of this approach is that the current adoptedStyleSheets JavaScript property would need to lift the “constructable” requirement for adoptedStyleSheets. This was recently agreed upon by the CSSWG but has not been implemented yet: Can we lift the restriction on constructed flag for adoptedStyleSheets?

One limitation of this approach is that shared styles that need to be applied exclusively to shadow roots (and not the main document) will need to include a CSS :host selector. This is not necessary for JavaScript-based adoptedStylesheets but will be necessary for declarative stylesheets, as there is currently no way in HTML to create stylesheets without applying them to the document they are defined in. This could also be addressed via a new type value on <style> tags and rel value on <link> tags, potentially “adopted-css”.

A challenge that arises is dealing with scopes and idrefs. If a declarative stylesheet can only be used within a single scope, it ends up being as limited as a regular <style> tag since it would need to be duplicated for every scope. A cross-scope idref system would enable nested shadow roots to access global stylesheets. This proposal recommends adding a new cross-scope ID xid attribute that SSR code would generate to be used with the first scope and referenced in later scope. See example in Declarative CSS Module Scripts

The script version of this already exists via the adoptedStyleSheets property:

import sheet from './styles.css' assert { type: 'css' }; // or new CSSStyleSheet();
shadowRoot.adoptedStyleSheets = [sheet];

Polyfills

Web developers often seek polyfills to allow them to use new web platform features while falling back gracefully in user agents where such features are not supported. A common strategy is to use JavaScript for polyfills. An example of this could be the following:

<script>
  function supportsDeclarativeAdoptedStyleSheets() {
    return document.createElement('template').shadowRootAdoptedStyleSheets != undefined;
  }

  if (!supportsDeclarativeAdoptedStyleSheets()) {
    // AdoptedStyleSheets is not supported on <template> - apply polyfill. This polyfill could be an injected <link> tag.
  }
</script>

There was also a suggestion for adding browser support to enable falling back to a normal <link> tag without the use of script, by binding the <link> tag's href attribute value to the CSS module identifier and adding a new attribute (noadoptedstylesheets) to avoid double-applying stylesheets.

This suggestion looks like the following:

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

Future Work

This proposal expands the concept of module specifiers to allow content in <style> elements to create named module map entries without referencing an external file. This concept could also apply to the <script> tag when inline module scripts are specified, giving the ability for these scripts to export values, something they are not currently capable of (see this issue).

<script type="module" specifier="exportsfoo">
const foo = 42;
export {foo};
</script>
<script type="module">
import {foo} from "exportsfoo";
...
</script>

Summary

The following table compares pros and cons of the various proposals:

Proposal Currently supported in DSD? Can hit network? FOUC Can apply styles only to shadow? Can export styles to parent document ?
1 Inline, declarative CSS Module Scripts ❌ No ✅ No ✅ No (unless module is imported from a separate file) Yes, on a per-sheet basis ✅ Yes
2 <link rel> ✅ Yes ❌ Yes ❌ Yes Yes, on a per-sheet basis ❌ No
3 @layer + importStyles ❌ No ✅ No ✅ No (unless @imports is used) Yes, on a per-sheet basis ❌ Not currently, but could be specified.
4 @Sheet ❌ No ✅ No ✅ No Yes, on a per-sheet basis ❌ Not currently, but could be specified.
5 adoptedstylesheets attribute ❌ No ✅ No ✅ No Yes, on a per-sheet basis ❌ No

Open issues

References and acknowledgements

Many thanks for valuable feedback and advice from other contributors: