MSEdgeExplainers

The shadowrootadoptedstylesheets Attribute for 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

This explainer is split from the Declarative adoptedStyleSheets for Sharing Styles In Declarative Shadow DOM explainer. That document covers both proposed features for declarative style sharing in Declarative Shadow DOM (DSD):

  1. <style type="module" specifier="..."> — Inline, declarative CSS module scripts that define reusable styles and add them to the module map.
  2. shadowrootadoptedstylesheets — An attribute on the <template> tag that adopts CSS modules into a shadow root's adoptedStyleSheets list.

This document focuses exclusively on the shadowrootadoptedstylesheets attribute (feature #2). While both features work best together, they can be released independently. For the full problem statement, goals, use cases, and details on <style type="module">, please see the parent explainer.

Proposal: The shadowrootadoptedstylesheets attribute

The shadowrootadoptedstylesheets attribute on the <template> element is a declarative analog to the imperative adoptedStyleSheets property. It accepts a space-separated list of module specifiers, and adopts the corresponding CSS module scripts into the shadow root's adoptedStyleSheets list.

Relationship to <style type="module">

<style type="module"> allows for declaratively creating CSS Module Scripts that can be applied via shadowrootadoptedstylesheets. See the parent explainer for details on how CSS modules are defined and registered.

Basic usage

Given a CSS module defined with <style type="module"> (see parent explainer), the styles can be applied to a DSD as follows:

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

<my-element>
  <template shadowrootmode="open" shadowrootadoptedstylesheets="foo">
    <div id="content">styled text</div>
  </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.

Multiple specifiers

The shadowrootadoptedstylesheets attribute accepts a space-separated list, allowing multiple stylesheets to be adopted:

<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">
    <div id="content">styled text</div>
  </template>
</my-element>

How the attribute is evaluated

When the <template> element is parsed, the shadowrootadoptedstylesheets attribute is evaluated. Each space-separated identifier is resolved using the following rules:

  1. Specifier is already in the module map: 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.
  2. Specifier resolves to a URL but is not in the module map: A fetch is initiated and an empty placeholder stylesheet is inserted into the adoptedStyleSheets list at the corresponding position. Once the fetch completes, the placeholder stylesheet is replaced with the fetched stylesheet. See Fetch Behavior For External Specifiers.
  3. Specifier does not resolve to a URL (e.g. a bare specifier with no import map entry): The fetch attempt fails. An empty placeholder stylesheet entry remains in the adoptedStyleSheets list and no styles are applied for that specifier. Developer tools should warn in this scenario.

Stylesheets are added in specified order, and applied as defined in CSS Style Sheet Collections. The attribute does not retroactively pick up declarative modules that are added to the module map after parsing shadowrootadoptedstylesheets (see Declarative modules are not applied retroactively for examples and details). However, external URL specifiers may be eventually applied — they trigger a fetch when not present in the module map, so styles will arrive once the fetch completes (with an associated FOUC).

This design allows for adopting both declarative and 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.

Declarative modules are not applied retroactively

Declarative modules (defined via <style type="module">) must be present in the module map at the time the <template> element is parsed — they are not applied retroactively.

In the following example, no styles are applied because the inline CSS module is defined after the <template>:

<my-element>
  <template shadowrootmode="open" shadowrootadoptedstylesheets="foo">
    ...
  </template>
</my-element>
<!-- This module definition comes too late — `shadowrootadoptedstylesheets` has already been processed. -->
<style type="module" specifier="foo">
  #content {
    color: red;
  }
</style>

When the <template> element is parsed, a fetch for the specifier "foo" with a module type of "css" is attempted. Because "foo" is a bare specifier, it does not resolve to a URL unless an import map provides a mapping for it. Without such a mapping, the fetch fails and the placeholder stylesheet entry remains empty. The later <style> element populates the module map, but since the attribute is not revisited after parsing, the adoptedStyleSheets list remains empty. Developer tools should warn in this scenario.

Similarly, when the <style> element is a child of the <template> that adopts it, the styles will not be applied to the shadow root:

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

The <template> element is parsed first and attempts to fetch "foo", which fails because the module map does not yet contain it. The child <style> element is parsed afterwards and populates the module map, but the attribute is not revisited. Subsequent <template> elements could adopt "foo", because after this point, it has been defined and is available in the module map.

For more details on the parsing workflow, including how <style type="module"> populates the module map via import map entries, see the Detailed Parsing Workflow section in the parent explainer.

Fetch Behavior For External Specifiers

When a specifier in shadowrootadoptedstylesheets is not present in the module map at parse time and the specifier resolves to a URL, the attribute initiates a fetch for that URL. An empty placeholder CSSStyleSheet entry is inserted into the adoptedStyleSheets array at the position corresponding to the specifier in shadowrootadoptedstylesheets. Once the fetch completes successfully, the placeholder stylesheet is replaced with the fetched CSSStyleSheet, and the shadow root's styles are updated accordingly.

This means the following example will work, even without a preceding <link rel="modulepreload">:

<my-element>
  <template shadowrootmode="open" shadowrootadoptedstylesheets="./foo.css">
    <div id="content">styled text</div>
  </template>
</my-element>

The shadow root is initially rendered without the styles from "foo.css". Once the fetch completes, the styles are applied. This will cause a FOUC (Flash of Unstyled Content) — the element is first painted without the external styles and then repainted once the fetch completes.

Developers should pre-fetch external CSS using <link rel="modulepreload"> to ensure it's in the module map before the <template> is parsed, avoiding FOUC and providing error handling:

<head>
  <link rel="modulepreload" as="style" href="./foo.css" onerror="handleError()">
</head>
...
<div>
  <template shadowrootmode="open" shadowrootadoptedstylesheets="./foo.css">
    ...
  </template>
</div>
<div>
  <template shadowrootmode="open" shadowrootadoptedstylesheets="./foo.css">
    ...
  </template>
</div>

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 attempt to pre-populate the module map before shadowrootadoptedstylesheets is parsed.

Limitations

The fetch fallback has an important limitation: there is no way to catch fetch errors or provide a fallback. If the fetch fails (e.g. a 404 response or network error), the placeholder stylesheet entry remains empty and no styles are applied for that specifier. There is no mechanism for the developer to detect this failure or substitute alternative styles declaratively.

For this reason, developer tools should surface a warning when shadowrootadoptedstylesheets triggers a fetch, recommending that developers either:

  1. Define the styles inline using <style type="module" specifier="..."> (a Declarative CSS Module) so the styles are available synchronously, or
  2. Use <link rel="modulepreload"> to pre-fetch the module, which supports error handling via the onerror event and can be combined with blocking="render" to avoid FOUC.

The order of shadowrootadoptedstylesheets reflects the order in the underlying adoptedStyleSheets array, which may impact the final application of CSS rules, as they are applied in array order. Since fetch completion order may not match the specified order, each fetch completion could trigger a separate FOUC.

Template element reflection

The <template> element that declares a Declarative Shadow DOM is consumed by the HTML parser — the parser creates the shadow root directly and the <template> element does not appear in the resulting DOM tree. This means that the shadowrootadoptedstylesheets attribute is no longer accessible via standard DOM APIs after parsing. To support reflection, a new shadowRootAdoptedStyleSheets DOM property should be added to the HTMLTemplateElement interface.

This property would reflect the initial value of the shadowrootadoptedstylesheets attribute as it was specified at parse time. It would return the space-separated string of specifiers that were originally provided, regardless of whether those specifiers resolved successfully. This is consistent with how other shadowroot* attributes on <template> are reflected (e.g. shadowRootMode).

const template = document.createElement('template');
template.setAttribute('shadowrootadoptedstylesheets', 'foo bar');
console.log(template.shadowRootAdoptedStyleSheets); // "foo bar"

This reflection is important for several reasons:

Alternate proposals

For a comprehensive list of alternate proposals for declarative style sharing (including Updates to Module Map Key, Local References For Link Rel, Layer and adoptStyles, and @Sheet), see the Alternate proposals section of the parent explainer. The following alternate proposals are specific to the mechanism used for adopting stylesheets into shadow roots.

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 DOM API, it more closely follows existing HTML semantics. It also allows for a rich set of features offered by the <link> element, such as error handling and media queries. However, there are several downsides to this approach.

One challenge of this approach is in ordering. Multiple stylesheets can be added to a shadow root's adopted stylesheet list with this proposal by including multiple <link> tags. <link> tags can be moved around in the DOM, which would imply that the order of adoptedStyleSheets would be updated accordingly. This could be complicated to keep in order if the underlying adoptedStyleSheets array is also modified externally. Alternatively, the adoptedStyleSheets array could not be re-ordered in response to these types of DOM changes, but that could be seen as confusing, because stylesheets applied by the existing <link rel="stylesheet"> tag are applied in DOM order. The shadowrootadoptedstylesheets attribute as specified accepts a fixed list of stylesheets, and thus is not subject to re-ordering complexity due to DOM mutations.

Another tradeoff with this approach is DOM bloat. Each adopted stylesheet would introduce another <link> tag in the DOM. With many stylesheets and many shadow roots, this could result in hundreds of extra DOM nodes, which the shadowrootadoptedstylesheets approach avoids entirely.

This approach could also introduce confusion for developers. There are already <link> tags used for styling via <link rel="stylesheet">, and stylesheet modules can already be preloaded via <link rel="modulepreload" as="style">. Adding a third variation on top of these existing patterns could add to this complexity.

For the Id-based alternate proposal (using HTML id attributes instead of module specifiers), see Id-based shadowrootadoptedstylesheets attribute on template in the parent explainer.

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>

This would behave similarly to shadowrootadoptedstylesheets, but without support for declarative modules.

Open issues

For additional open issues related to <style type="module"> and the broader declarative style sharing proposal, see the Open issues section of the parent explainer.

References and acknowledgements

Many thanks for valuable feedback and advice from other contributors: