Constructable Stylesheet Objects

A Collection of Interesting Ideas,

This version:
https://wicg.github.io/construct-stylesheets/
Issue Tracking:
GitHub
Editors:
Tab Atkins Jr. (Google)
(Google)
(Google)

Abstract

This draft defines additions to CSSOM to make CSSStyleSheet objects directly constructable, along with a way to use them in DocumentOrShadowRoots.

1. Motivation

Most web components uses Shadow DOM. For a style sheet to take effect within the Shadow DOM, it currently must be specified using a style element within each shadow root. As a web page may contain tens of thousands of web components, this can easily have a large time and memory cost if user agents force the style sheet rules to be parsed and stored once for every style element. However, the duplications are actually not needed as the web components will most likely use the same styling, perhaps one for each component library.

Some user agents might attempt to optimize by sharing internal style sheet representations across different instances of the style element. However, component libraries may use JavaScript to modify the style sheet rules, which will thwart style sheet sharing and have large costs in performance and memory.

2. Proposed Solution

We are proposing to provide an API for creating stylesheet objects from script, without needing style elements, and also a way to reuse them in multiple places. Script can optionally add, remove, or replace rules from a stylesheet object. Each stylesheet object can be added directly to any number of shadow roots (and/or the top level document).

const myElementSheet = new CSSStyleSheet();
class MyElement extends HTMLElement {
  constructor() {
    super();
    const shadowRoot = this.attachShadow({ mode: "open" });
    shadowRoot.adoptedStyleSheets = [myElementSheet];
  }
  
  connectedCallback() {
    // Only actually parse the stylesheet when the first instance is connected.
    if (myElementSheet.cssRules.length == 0) {
       myElementSheet.replaceSync(styleText);
    }
  }
}

3. Constructing Stylesheets

[Constructor(optional CSSStyleSheetInit options)]
partial interface CSSStyleSheet {
  Promise<CSSStyleSheet> replace(USVString text);
  void replaceSync(USVString text);
};

dictionary CSSStyleSheetInit {
  (MediaList or DOMString) media = "";
  DOMString title = "";
  boolean alternate = false;
  boolean disabled = false;
};
CSSStyleSheet(options)
When called, execute these steps:
  1. Construct a new CSSStyleSheet object sheet, with location set to the base URL of the Document for the Window where this constructor is called on, no parent CSS style sheet, no owner node, no owner CSS rule, and title set to the title attribute of options.

  2. Set sheet’s origin-clean flag. Set sheet’s constructed flag. Set sheet’s constructor document to the Document for the Window where this constructor is called on.

  3. If the media attribute of options is a string, create a MediaList object from the string and assign it as sheet’s media. Otherwise, assign a copy of the value of the attribute as sheet’s media.

  4. If the alternate attribute of options is true, set sheet’s alternate flag.

  5. If the disabled attribute of options is true, set sheet’s disabled flag.

  6. Return sheet.

CSSStyleSheet instances have the following associated states:

constructed flag
Specified when created. Either set or unset. Unset by default. Signifies whether this stylesheet is made via constructor or not, so must be set only for stylesheets that are constructed using the CSSStyleSheet() or CSSStyleSheet(options) function.
disallow modification flag
Either set or unset. Unset by default. If set, modification to the stylesheet’s rules are not allowed.
constructor document
Specified when created. The Document where the stylesheet is originally constructed on. Null by default.

4. Modifying Constructed Stylesheets

After construction, constructed stylesheets can be modified using rule modification methods like insertRule(rule[, index]) or deleteRule(index), or replace(text) and replaceSync(text) if the sheet’s disallow modification flag is not set. If those methods are called when the sheet’s disallow modification flag is set, or insertRule(rule) is used to add an @import rule, a "NotAllowedError" DOMException will be thrown as detailed in the below algorithms.

insertRule(rule, index)
  1. Let sheet be the stylesheet on which this function is called on.

  2. If sheet’s constructed flag and disallow modification flag is set, throw "NotAllowedError" DOMException.

  3. Parse a rule from rule. If the result is an @import rule and sheet’s constructed flag is set, throw "NotAllowedError" DOMException.

  4. (The rest of the algorithm remains as in CSSOM)

deleteRule(index)
  1. Let sheet be the stylesheet on which this function is called on.

  2. If sheet’s constructed flag and disallow modification flag is set, throw "NotAllowedError" DOMException.

  3. (The rest of the algorithm remains as in CSSOM)

replace(text)
  1. Let sheet be the stylesheet on which this function is called on.

  2. If sheet’s constructed flag is not set, or sheet’s disallow modification flag is set, throw a "NotAllowedError" DOMException.

  3. Set sheet’s CSS rules to an empty list, and set sheet’s disallow modification flag.

  4. Let promise be a promise.

  5. In parallel, do these steps:

    1. Let rules be the result of running parse a list of rules from text. If rules is not a list of rules (i.e. an error occurred during parsing), set rules to an empty list.

    2. Wait for loading of @import rules in rules and any nested @imports from those rules (and so on).

      Note: Loading of @import rules should follow the rules used for fetching style sheets for @import rules of stylesheets from <link> elements, in regard to what counts as success, CSP, and Content-Type header checking.

      Note: We will use the fetch group of sheet’s constructor document's relevant settings object for @import rules and other (fonts, etc) loads.

      Note: The rules regarding loading mentioned above are currently not specified rigorously anywhere.

  6. Return promise.

replaceSync(text)
When called, execute these steps:
  1. Let sheet be the stylesheet on which this function is called on.

  2. If sheet’s constructed flag is not set, or sheet’s disallow modification flag is set, throw a "NotAllowedError" DOMException.

  3. Set sheet’s CSS rules to an empty list.

  4. Parse a list of rules from text. If it returned a list of rules, assign the list as sheet’s CSS rules.

  5. If sheet contains one or more @import rules, throw a "NotAllowedError" DOMException.

  6. Return sheet.

5. Using Constructed Stylesheets

partial interface DocumentOrShadowRoot {
  attribute FrozenArray<CSSStyleSheet> adoptedStyleSheets;
};
adoptedStyleSheets, of type FrozenArray<CSSStyleSheet>
On getting, adoptedStyleSheets returns this DocumentOrShadowRoot's adopted stylesheets.

On setting, adoptedStyleSheets performs the following steps:

  1. Let adopted be the result of converting the given value to a FrozenArray<CSSStyleSheet>

  2. If any entry of adopted has its constructed flag not set (e.g. it’s not made by factory methods to construct stylesheets), throw a "NotAllowedError" DOMException.

  3. Set this DocumentOrShadowRoot's adopted stylesheets to adopted.

Every DocumentOrShadowRoot has adopted stylesheets.

The user agent must include all style sheets in the DocumentOrShadowRoot's adopted stylesheets whose constructor document is the same as the DocumentOrShadowRoot's node document inside its document or shadow root CSS style sheets.

These adopted stylesheets are ordered after all the other style sheets (i.e. those derived from styleSheets).

Note that because the adopted stylesheets are a property of the DocumentOrShadowRoot, they move along with the ShadowRoot if it gets adopted into a different Document, e.g. when adopting its shadow host. However, only adopted stylesheets that have the constructor document equal to the new Document will be applied, which means that a constructed CSSStyleSheet is only applicable in the document tree of its constructor document.

Conformance

Conformance requirements are expressed with a combination of descriptive assertions and RFC 2119 terminology. The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in the normative parts of this document are to be interpreted as described in RFC 2119. However, for readability, these words do not appear in all uppercase letters in this specification.

All of the text of this specification is normative except sections explicitly marked as non-normative, examples, and notes. [RFC2119]

Examples in this specification are introduced with the words “for example” or are set apart from the normative text with class="example", like this:

This is an example of an informative example.

Informative notes begin with the word “Note” and are set apart from the normative text with class="note", like this:

Note, this is an informative note.

Index

Terms defined by this specification

Terms defined by reference

References

Normative References

[CSS-CASCADE-4]
Elika Etemad; Tab Atkins Jr.. CSS Cascading and Inheritance Level 4. 28 August 2018. CR. URL: https://www.w3.org/TR/css-cascade-4/
[CSS-SYNTAX-3]
Tab Atkins Jr.; Simon Sapin. CSS Syntax Module Level 3. 20 February 2014. CR. URL: https://www.w3.org/TR/css-syntax-3/
[CSSOM-1]
Simon Pieters; Glenn Adams. CSS Object Model (CSSOM). 17 March 2016. WD. URL: https://www.w3.org/TR/cssom-1/
[DOM]
Anne van Kesteren. DOM Standard. Living Standard. URL: https://dom.spec.whatwg.org/
[FETCH]
Anne van Kesteren. Fetch Standard. Living Standard. URL: https://fetch.spec.whatwg.org/
[HTML]
Anne van Kesteren; et al. HTML Standard. Living Standard. URL: https://html.spec.whatwg.org/multipage/
[RFC2119]
S. Bradner. Key words for use in RFCs to Indicate Requirement Levels. March 1997. Best Current Practice. URL: https://tools.ietf.org/html/rfc2119
[WebIDL]
Cameron McCormack; Boris Zbarsky; Tobie Langel. Web IDL. 15 December 2016. ED. URL: https://heycam.github.io/webidl/

IDL Index

[Constructor(optional CSSStyleSheetInit options)]
partial interface CSSStyleSheet {
  Promise<CSSStyleSheet> replace(USVString text);
  void replaceSync(USVString text);
};

dictionary CSSStyleSheetInit {
  (MediaList or DOMString) media = "";
  DOMString title = "";
  boolean alternate = false;
  boolean disabled = false;
};

partial interface DocumentOrShadowRoot {
  attribute FrozenArray<CSSStyleSheet> adoptedStyleSheets;
};