1. Introduction
This is a diff spec over CSS Scoping Module Level 1. It is currently an Exploratory Working Draft: if you are implementing anything, please use Level 1 as a reference. We will merge the Level 1 text into this draft once it reaches CR.
2. Default Styles for Custom Elements
CSS Scoping 1 § 2 Default Styles for Custom Elements
3. Shadow Encapsulation
CSS Scoping 1 § 3 Shadow Encapsulation
4. Scoped Styles
A scope is a subtree or fragment of a document, which can be used by selectors for more targeted matching. Scopes are described in CSS through a combination of two selector lists:
-
The <scope-start> is a <forgiving-selector-list>. Each element matched by <scope-start> is a scoping element, creating a scope with itself as the scoping root.
-
The <scope-end> is a <forgiving-selector-list> that is scoped by the <scope-start> selector, with the scoping roots as :scope elements. Each element matched by <scope-end> is a scope boundary. Scope boundary elements provide lower bounds to a scope, so that scoped selectors are not able to match more deeply nested elements.
Note: This means that the :scope pseudo-class in a lower boundary (<scope-end>) selector will always refer to the scope being described, rather than the outer scope context.
/* .content is only a boundary when the :scope is inside .sidebar */ @scope ( .media-object) to( .sidebar :scope .content) { img{ border-radius : 50 % ; } }
Or based on a specific relationship to the scoping root:
/* .content is only a boundary when it is a direct child of the :scope */ @scope ( .media-object) to( :scope > .content) { img{ border-radius : 50 % ; } }
Each resulting scope includes a scoping root and all its descendants, up to and including any scope boundary elements, but not the descendants of those boundaries.
In contrast to Shadow Encapsulation, which describes a persistent one-to-one relationship in the DOM between a shadow host and its nested shadow tree, multiple overlapping scopes can be defined in relation to the same elements.
@scope ( .light-scheme) { a{ color : darkmagenta; } } @scope ( .dark-scheme) { a{ color : plum; } } @scope ( .media-object) { .media-image{ border-radius : 50 % ; } .media-content{ padding : 1 em ; } }
@scope ( .media-object) to( .content) { img{ border-radius : 50 % ; } /* it is also possible to style the lower boundary itself */ .content{ padding : 1 em ; } }
The img selector will only match image tags that are inside a .media-object, without any intervening .content class between it and the scoping root.
4.1. The in-scope pseudo-class :in()
The in-scope pseudo-class, :in(), is a functional pseudo-class with the following syntax:
:in(<scope-start> [/ <scope-end>]?)
If, after parsing, <scope-start> is an empty list, the pseudo-class is valid but matches nothing, and defines no scopes. Otherwise, the pseudo-class matches any element that is in a scope described by the given <scope-start> and <scope-end> selectors.
Note: This does not effect the :scope elements for the selector.
The specificity of the :in() pseudo-class is replaced by the specificity of the most specific complex selector in its <scope-start> argument.
.title:in ( .post / .comments) { font-size : 2 em ; }
Without any such lower boundaries, the in() pseudo-class is similar to other existing selectors. These three selectors will all select the same elements, with the same specificity:
.child:in ( .ancestor) { color : darkmagenta; } .child:is ( .ancestor, .ancestor *) { color : darkmagenta; } .ancestor.child, .ancestor .child{ color : darkmagenta; }
4.2. Scoping Styles in CSS: the @scope rule
The @scope block at-rule allows authors to create scoped stylesheets in CSS, with the addition of scope proximity weighting in the cascade. The syntax of the @scope rule is:
@scope (<scope-start>) [to (<scope-end>)]? { <stylesheet> }
The @scope rule has three primary effects on the style rules in <stylesheet>. For each scope that is described by the given <scope-start> and <scope-end>:
-
Selectors are scoped to the scope in question, with the :scope element being the scoping root.
-
Selectors are given the added specificity of the most specific complex selector in the <scope-start> argument. This is designed to match the behavior of the :in() and :is() selectors.
-
The cascade prioritizes rules with a more proximate scoping root, regardless of source order.
#hero img{ border-radius : 50 % ; } @scope ( #hero) { img{ border-radius : 50 % ; } }
main-component
and sub-component
)
and every element is marked as part of one or both scopes
using the data-scope
attribute:
< section data-scope = "main-component" > < p data-scope = "main-component" > ...< p > <!-- sub-component root is in both scopes --> < section data-scope = "main-component sub-component" > <!-- children are only in the inner scope --> < p data-scope = "sub-component" > ...< p > </ section > </ section >
Those custom scope attributes are then appended to every single selector in CSS:
p[ data-scope~='main-component' ] { color : red; } p[ data-scope~='sub-component' ] { color : blue; } /* both sections are part of the outer scope */ section[ data-scope~='main-component' ] { background : snow; } /* the inner section is also part of the inner scope */ section[ data-scope~='sub-component' ] { color : ghostwhite; }
Using the @scope rule, authors and tools can replicate similar behavior with the unique attribute or class applied only to the scoping roots:
< section data-scope = "main-component" > < p > ...< p > < section data-scope = "sub-component" > <!-- children are only in the inner scope --> < p > ...< p > </ section > </ section >
Then the class or attribute can be used for establishing both upper and lower boundaries, such that scopes only overlap at those boundaries:
@scope ([ data-scope='main-component' ]) to([ data-scope]) { p{ color : red; } /* both sections are part of the outer scope */ section{ background : snow; } } @scope ([ data-scope='sub-component' ]) to([ data-scope]) { p{ color : blue; } /* the inner section is also part of the inner scope */ section{ color : ghostwhite; } }
@scope rules can be nested. In this case, just as with the nested style rules, the selectors of the inner @scope are scoped by the selectors of the outer one.
4.2.1. Scope Proximity in the Cascade
This likely belongs in the css-cascade specification.
Scope proximity is considered in the cascade sort order after specificity, and before order of appearance.
If the :scope elements of two declarations have an ancestor/descendant relationship, then the declaration whose :scope element is the descendant wins.
Note: When the :scope element is not otherwise defined for a declaration, it is the document root element.
@scope ( .light-scheme) { a{ color : darkmagenta; } } @scope ( .dark-scheme) { a{ color : plum; } }
If light-scheme and dark-scheme classes are nested in the DOM, whichever is closer to a given link in the DOM tree will take precedence for styling that link, regardless of their order of appearance in CSS:
< section class = "light-scheme" > < a href = "#" > light scope: darkmagenta link color</ a > < aside class = "dark-scheme" > < a href = "#" > both scopes, but dark-scheme is a closer ancestor: plum link color</ a > </ aside > </ section >
5. Changes
5.1. Additions Since Level 1
The following features have been added since Level 1:
-
The definition of a scope, as described by a combination of <scope-start> and <scope-end> selectors.
-
The in-scope (:in()) pseudo-class for selecting with lower-boundaries
-
The @scope rule for creating scoped stylesheets
-
The definition of scope proximity in the cascade
Acknowledgments
Elika J. Etemad / fantasai, Giuseppe Gurgone, Keith Grant, Lea Verou, Nicole Sullivan, and Theresa O’Connor contributed to this specification.
6. Privacy and Security Considerations
This specification introduces Shadow DOM and some shadow-piercing capabilities,
but this does not introduce any privacy or security issues—