CSS Scope Proposal & Explainer

Table of Contents
Archived:

🚚 This content is being maintained elsewhere. Our notes are likely out-of-date.

See the latest specification for details.

Authors

Miriam Suzanne

Participate

Please leave any feedback on the CSSWG issues for this proposal:

Typos or other document-specific issues can be reported in this repo.

Introduction

There are many overlapping and sometimes contradictory features that can live under the concept of “scope” in CSS – but they divide roughly into two approaches:

  1. Total isolation of a component DOM subtree/fragment from the host page, so that no selectors get in or out unless explicitly requested.
  2. Lighter-touch, style-driven namespacing, and prioritization of “proximity” when resolving the cascade.

That has lead to a wide range of proposals over the years, including a scope specification that was never implemented. Focus moved to Shadow-DOM, which is mainly concerned with approach #1 – full isolation. Meanwhile authors have attempted to handle approach #2 through convoluted naming conventions (like BEM) and JS tooling (such as CSS Modules, Styled Components, & Vue Scoped Styles).

This document is proposing a native CSS approach for what many authors are already doing with those third-party tools & conventions.

Goals

The namespace problem

All CSS Selectors are global, matching against the entire DOM. As projects grow, or adapt a more modular “component-composition” approach, it can be hard to track what names have been used, and avoid conflicts.

To solve this, authors rely on convoluted naming conventions (BEM) and JS tooling (CSS Modules & Scoped Styles) to “isolate” selector matching inside a single “component”.

BEM helps authors by ensuring that only component “blocks” need unique naming:

.media { /* block */ }
.tabs { /* block */ }

Meanwhile, any internal “elements” or “modifiers” will be scoped to the block:

.media--reverse { /* modifier */ }
.media__img { /* element */ }
.media__text { /* element */ }

.tabs--left { /* modifier */ }
.tabs__list { /* element */ }
.tabs__panel { /* element */ }

The nearest-ancestor “proximity” problem

Ancestor selectors allow us to filter the “scope” of nested selectors to a sub-tree in the DOM:

/* link colors for light and dark backgrounds */
.light-theme a { color: purple; }
.dark-theme a { color: plum; }

But problems show up quickly when you start thinking of these as modular styles that should nest in any arrangement.

<div class="dark-theme">
<a href="#">plum</a>

<div class="light-theme">
<a href="#">also plum???</a>
</div>
</div>

Our selectors appropriately have the same specificity, but they are not weighted by “proximity” to the element being styled. Instead we fallback to source order, and .dark-theme will always take precedence.

There is no selector/specificity solution that accurately reflects what we want here – with the “nearest ancestor” taking precedence.

This was one of the original issues highlighted by OOCSS in 2009.

The lower-boundary, or “ownership” problem (aka “donut scope”)

While “proximity” is loosely concerned with nesting styles, the problem comes into more focus with the concept of modular components – which can be more complex.

To use BEM terminology, Components are generally comprised of:

In html templating languages, and JS frameworks, this can be represented by an “include” or “single file component”.

BEM attempts to convey this “ownership” in CSS:

/* any title inside the component tree */
.component .title { /* too broad */ }

/* only a title that is a direct child of the component */
.component > .title { /* too limiting of DOM structures */ }

/* just the title of the component */
.component__title { /* just right? */ }

Nicole Sullivan coined the term “donut” scope for this issue in 2011 – because the scope can have a hole in the middle. It would be useful for authors to express this DOM-fragment “ownership” more clearly in native HTML/CSS.

CSS Modules, Vue, Styled-JSX, and other tools often use a similar pattern (with slight variations to syntax) – where “scoped” selectors only apply to the locally described DOM fragment, and not descendants.

In Vue single file components, authors can write html templates with “scoped” style blocks:

<!-- component.vue -->
<template>
<section class="component">
<div class="element">...<div>
<sub-component>...</sub-component>
</section>
</template>

<style scoped>
.component { /* ... */ }
.element { /* ... */ }
.sub-component { /* ... */ }
</style>

<!-- sub-component.vue -->
<template>
<section class="sub-component">
<div class="element">...<div>
</section>
</template>

<style scoped>
.sub-component { /* ... */ }
.element { /* ... */ }
</style>

While the language is similar to shadow-DOM in many ways, the output is quite different – and much less isolated. The components remain part of the global scope, and only the explicitly “scoped” styles are contained. That’s often achieved by adding automated unique attributes to each element based on the component(s) it belongs to:

<section class="component" scope="component">
<div class="element" scope="component">...<div>

<!-- nested component "shell" is in both scopes -->
<section class="sub-component" scope="component sub-component">
<div class="element" scope="sub-component">...<div>
</section>
</section>

And matching attributes are added to each selector:

/* component.vue styles after scoping */
.component[scope=component] { /* ... */ }
.element[scope=component] { /* ... */ }
.sub-component[scope=component] { /* ... */ }

/* sub-component.vue styles after scoping */
/* note that both style `.element` without any overlap or naming conflicts */
.sub-component[scope=sub-component] { /* ... */ }
.element[scope=sub-component] { /* ... */ }

Non-goals

There is a more extreme isolation use-case. It’s mostly used for “widgets” that will appear unchanged across multiple projects – but sometimes also in component libraries on larger projects.

Full isolation blocks off a fragment of the DOM, so that it only accepts styles that are explicitly scoped. General page styles do not apply.

We don’t think this is the most common concern for authors, but it has received the most attention. Shadow DOM is entirely constructed around this behavior.

We have not attempted to address that form of scope in this proposal – it feels like a significantly different approach that already has work underway.

See Yu Han’s proposals for building on shadow DOM below.

Proposed Solution

In the long-standing “Bring Back Scope” issue-thread, Giuseppe Gurgone suggests a syntax building on the original un-implemented @scope spec, but adding a lower boundary:

@scope (from: .carousel) and (to: .carousel-slide-content) {
p { color: red }
}

That seems like a good start, but we’ve worked to simplify and clarify where possible.

Defining scopes: the @scope rule

The CSSWG has resolved to put this work in CSS Cascading and Inheritance Level 6, and rename CSS Scoping to be about Shadow-DOM specifically.

The current syntax proposal for @scope is:

@scope [(<scope-start>)]? [to (<scope-end>)]? {
<stylesheet>
}

This approach is designed to keep scoping confined to CSS (no need for a new HTML attribute), flexible (scopes can overlap as needed), and non-invasive (global styles continue to work as expected). Existing tools would still be able to provide syntax sugar for single-file components – automatically generating the @scope rule – but move the primary functionality into CSS.

The @scope prelude syntax is made up of two parts, a ‘scope start’ and ‘scope end’ clause.

Defining the scope root: <scope-start>

The (<scope-start>) clause, defines the root of the scope with a forgiving selector list:

@scope (.media-block) {
img { border-radius: 50%; }
}

That creates a ‘scoped’ tree fragment that starts from any matched scope root element. Both the matched element and all its descendants are in the generated scope.

The scope root selector is optional when styles are nested in the DOM. This allows a DOM-nested stylesheet to implicitly scope styles, using the direct parent ‘owner’ node of the <style> element (or the containing tree for constructable stylesheets with no owner node):

<div>
<style>
@scope {
p { color: red; }
}
</style>
<p>this is red</p>
</div>
<p>not red</p>

That would be equivalent to:

<div id="foo">
<style>
@scope (#foo) {
p { color: red; }
}
</style>
<p>this is red</p>
</div>
<p>not red</p>

The only difference is in the resulting selector specificity, which we describe in more detail below.

Defining scope boundaries: <scope-end>

The optional scope-end clause accept a list of selectors that represent lower-boundary “slots” in the scope. The <scope-end> clause is a forgiving selector list, which is ‘scoped’ by the <scope-start> clause. The targeted lower-boundary elements are excluded from the scope, along with all their descendants:

@scope (.media-block) to (.content) {
img { border-radius: 50%; }
}

The above example would only match img elements that are inside .media-blockbut not if there are intervening .content elements between the scope root and selector target:

<div class="media-block">
<img src="..."><!-- this img is in the .media-block scope -->
<div class="content"><!-- this ends the scope -->
<img src="..."><!-- this img is NOT in scope -->
</div>
</div>

Authors can write ‘inclusive’ lower boundaries by appending > * to scope-end selectors. In the example above, if the .content element should be included in the scope, while excluding its descendants, the rule would become:

@scope (.media-block) to (.content > *) {
.content { /* This is now in scope */ }
}

Nesting scopes

When @scope rules are nested, the inner <scope-start> selectors are ‘scoped’ by the outer scope definition. This can be used to narrow down a scope fragment. However, further nested selectors still only have a single scope defined by a single scope-root and lower scope boundaries.

Note: This is not the same as the intersection of scopes, since the order is relevant. Each nested scope is ‘scoped’ by the previous, and can only match if the new scope root is ‘in’ the previous scope.

Selecting a scope root: the :scope pseudo-class

(This section matches the proposed resolution of issue #8377, rather than the current specification)

The existing Reference Element Pseudo-class (:scope) can be used in a scoped selector to reference the scoping element.

When not explicitly used in a scoped selector, the :scope pseudo is an implied ancestor:

@scope (.media) {
img { /* :scope img */ }
.content { /* :scope .content */ }
}

However, authors can establish an explicit or different relationship to the scope root by including either :scope or & in the selector. These scope-containing selectors do not get the implicit ancestor prepended:

@scope (.media) {
:scope img { /* :scope img */ }
:scope .content { /* :scope .content */ }
}

@scope (.light-mode .media) {
:scope { /* :scope */ }
:root :scope > img { /* :root :scope > img */ }
.sidebar:scope { /* .sidebar:scope */ }
}

Note that scope-containing selectors are able to reference the entire DOM tree. Only the matched subject of the selector is required to be in scope.

In many ways, the :scope selector behaves much like the & in CSS Nesting, or the functional pseudo-class :is(<scope-root>). However, unlike the nesting & or the :is() pseudo, the :scope selector refers specifically to the root element of a scope - so the following will not match nested .media elements, even if they are in-scope:

@scope (.media) {
:scope :scope { /* no match */ }
}

Instead, we could use any of the following:

@scope (.media) {
/* select nested .media inside the scope */
.media { background: gray; }
:scope .media { background: gray; }
& & { background: gray; }
:scope & { background: gray; }
}

Scope in the cascade

The @scope rule has a double-impact on the cascade of scoped selectors – as part of specificity, and then again in relation to proximity.

Scope Specificity

While ‘scope’ and ‘specificity’ have some overlap – and both impact on the priority of a declaration – we believe they should be handled separately in the cascade. Scope selectors are used for the purpose of scoping, and should not have their specificity added to the selectors that they scope.

A small amount of (class-level) specificity can be added with explicit use of :scope, and the full weight of the scoping selectors can be added via the & selector:

@scope (#media) {
img { /* [0, 0, 1] */ }
:scope img { /* [0, 1, 1] */ }
& img { /* [1, 0, 1] */ }
}

This provides flexibility for authors without increasing specificity by default.

Some alternative approaches are discussed below.

Scope Proximity

The scope syntax itself solves the issue of naming-conflicts, with lower-boundaries and ownership. But the issue of proximity requires a change in the Cascade. For that, we’re introducing a new step in the Cascade Sorting logic, called scope proximity.

The proximity of a selector is a single number representing the ‘generational’ distance between a matched selector subject and the selector’s scope root in the DOM tree.

(Note that nested scopes still result in a single root)

A direct parent/child relationship has a proximity of 1, while further nested subjects will have higher proximity values. If the subject of a selector is itself the scope root, then the proximity is 0. If no scope root is defined, the proximity value is infinite.

In the cascade scope proximity step of the cascade, lower proximity selectors have precedence over higher proximity selectors:

@scope (.light-theme) {
a { color: purple; }
}

@scope (.dark-theme) {
a { color: plum; }
}
<div class="dark-theme">
<a href="#">plum</a>

<div class="light-theme">
<a href="#">purple</a>
</div>
</div>

While the more nested link is matched by both selectors, the selector with ‘lower proximity scope’ (the .light-theme root) is applied.

Given the same proximity of multiple scopes, source order would continue to be the final cascade filter:

@scope (.light-theme) {
a { color: purple; }
}

@scope (.special-links) {
a { color: maroon; }
}
<div class="special-links light-theme">
<a href="#">maroon</a>
</div>

There has been some debate about the priority of cascade proximity in relation to selector specificity.

Since both specificity and proximity are heuristics, it’s not clear which one is a ‘better’ approximation of author intent. The more common preference has been for a weak proximity – since that leaves authors with more tools for resolving conflicts in either direction, through layering, or specificity adjustments. This is also the behavior applied by the existing browser prototype.

That means, currently, specificity will override scope proximity. Given the following CSS, a paragraph matched by both rules would be red, thanks to the added specificity:

@scope aside {
p { color: green; }
}

aside#sidebar p { color: red; }

This is a major departure from both the original scope specification and Shadow-DOM encapsulation context, which override specificity.

We discuss this in more detail below.

Key scenarios

Avoid naming conflicts without custom conventions

Authors currently rely on intricate naming conventions to avoid duplicate naming within components:

.article__title {
font-size: 2em;
}
.article__meta {
font-size: 2em;
}

.form__title {
font-weight: bold;
}
.form__meta {
font-weight: bold;
}

Sometimes authors will automate that process, and group names visually, using nested syntax in a pre-processor like Sass:

.article {
&__title { font-size: 2em; }
&__meta { font-style: italic; }
}

.form {
&__title { font-weight: bold; }
&__meta { text-align: center; }
}

This syntax would provide a uniform solution that is native to CSS. Authors can reduce naming conflicts across CSS “components”/“objects” by scoping internal selectors so they only match within a particular block:

@scope (article) {
.title { font-size: 2em; }
.meta { font-style: italic; }
}

@scope (form) {
.title { font-weight: bold; }
.meta { text-align: center; }
}

Express ownership boundaries in nested components

By adding lower boundaries or “slots” to the scope, ownership becomes more clear when the scopes are nested.

Using the example above, we can allow comment forms to be nested inside articles while continuing to maintain the distinction between article-elements and form-elements:

@scope (article) to (.comments) {
.title { font-size: 2em; }
.meta { font-style: italic; }
}

@scope (.comments) {
.title { font-weight: bold; }
.meta { text-align: center; }
}

Recursive nesting with ownership

This can also be useful when applying modifiers to components that might nest indefinitely – such as the popular “media-object” containing a fixed image and responsive content that flows around it. Modifiers can be added to an outer component without impacting nested components of the same type.

For example, nested “media objects” of different types:

<div class="media reverse">
<img src="...">
<div class="content">
<div class="media">
<img src="...">
<div class="content">
</div>
</div>
</div>
</div>

Rather than adding the .reverse modifier to every element in the outer media block, we can scope the effects of the modifier. This could be done in a variety of ways, but may be a good use-case for nesting scopes:

@scope (.media) to (.content) {
/* only the inner image */
@scope (:scope:not(.reverse)) {
img { margin-right: 1em; }
}

/* only the outer image */
@scope (:scope.reverse) {
img { margin-left: 1em; }
}
}

Recognizing proximity of nested components without lower-bounds

As demonstrated above, authors could establish scope precedence even when lower bounds are not required. For example, light and dark themes that can be nested in any arrangement:

@scope (.light-theme) {
a { color: purple; }
}

@scope (.dark-theme) {
a { color: plum; }
}
<div class="dark-theme">
<a href="#">plum</a>

<div class="light-theme">
<a href="#">purple</a>

<div class="dark-theme">
<a href="#">plum again</a>
</div>
</div>
</div>

JS tools & “single file components”

Existing tools could move to automating this syntax over time, rather than using custom attributes, since the result is very similar. Without any changes visible to the user, output that currently looks like:

/* component.vue styles after scoping */
.component[scope=component] { /* ... */ }
.element[scope=component] { /* ... */ }

/* sub-component.vue styles after scoping */
/* note that both style `.element` without any overlap or naming conflicts */
.sub-component[scope=sub-component] { /* ... */ }
.element[scope=sub-component] { /* ... */ }

Could be converted to:

/* component.vue styles after scoping */
@scope (.component) to (.sub-component) {
:scope { /* ... */ }
.element { /* ... */ }
}

/* sub-component.vue styles after scoping */
@scope (.sub-component) {
:scope { /* ... */ }
.element { /* ... */ }
}

Detailed design discussion & alternatives

Should we be building on Shadow DOM?

This is often the first question asked of any scope-related proposal, for good reason. Shadow-DOM was designed for the express purpose of encapsulating styles & behavior.

But “scope” can describe a number of sometimes-contradictory use-cases, which can’t all be solved using the same primitives. Shadow-DOM starts from a few assumptions that make sense in very specific use-cases, but do not describe the majority of web design:

Yu Han has an interesting proposal in two parts, designed to improve shadow-DOM, by providing some more flexibility:

  1. Allow shadow-DOM elements to opt-in to global styles
  2. Allow light-DOM elements to opt-in to style isolation

Those proposals (along with declarative shadow-DOM) would be very helpful for making strong isolation more flexible for authors. But even with that added flexibility, many of the assumptions & limitations remain.

Shadow-DOM is not designed for the common “design system” approach, where patterns overlap in more fluid ways, and global styles are expected to “flow through” and tie all the pieces together.

Are scope attributes useful in html?

Many scope proposals include a scope attribute in HTML. That’s required for the full-isolation use-case – where elements need to opt-in or out of global page styles up-front. But following that path would make scope much more similar to shadow-DOM in it’s limitations:

Some authors might appreciate the “automatic” nature of that approach, but they could achieve the same goals with a naming convention or tooling:

p { color: blue; }

@scope ([data-scope=main]) to ([data-scope]) {
p { color: green; }
}
@scope ([data-scope=note]) to ([data-scope]) {
p { color: gray; }
}
<p>This text is blue</p>
<section data-scope="main">
<p>This text is green</p>
<div data-scope="note">
<p>This text is gray</p>
</div>
</section>
<div data-scope="note">
<p>This text is gray</p>
</div>

Do we need special handling around the shadow-DOM?

This proposal would have no impact on existing shadow DOM behavior. Scoped styles can be used in both light and shadow DOM. Both scopes & scoped styles respect the shadow boundary, like any other CSS rules.

There are still some questions raised by Rune Lillesveen about what selectors would be allowed inside a shadow-DOM encapsulated scope rule:

For selectors in @scope rules in shadow trees, we should figure out which restrictions apply wrt matching elements outside the shadow tree. @scope rules in shadow trees should not be able to target elements outside the shadow tree, but what about :host/:host-context?

A JS API for fetching “donut scope” elements?

Given a tree fragment, JS is able to match elements within a donut scope:

const matches = root.querySelectorAll(
"*:not(:scope boundary, :scope boundary *)"
);

With CSS nesting, it would also be possible to use the @scope rule directly:

document.querySelector("@scope (root) to (boundary) { * }");

But those solutions do not provide any way to select and capture the “donuts” themselves, only select elements inside the donut.

Rather than adding a new selector format, the CSSWG has defined a second “exclusions” parameter on methods like querySelector() and querySelectorAll():

document.querySelector("root", "boundary");

This has been agreed on by the CSSWG.

How does scope interact with the nesting module?

Conceptually, there is a fair amount of overlap between CSS ‘scoping’ and some use-cases for CSS Nesting.

Scope can be used for a subset of nesting use-cases, where the nested-selector subject is a descendant of the parent-selector subject. In those cases, scope provides additional benefits, such as lower boundaries and proximity weighting.

Ideally, we should allow authors to combine these two approaches, or move smoothly between them.

In the nesting syntax, for example, we would allow the @scope rule to be nested inside an existing selector block. The scope rule could establish a scope-root based on the parent selector, using &. These examples would have the same meaning:

/* explicit scope with root */
@scope (.media) to (.content) {
img { object-fit: cover; }
}

.media {
/* nested scope with explicit nesting root */
@scope (&) to (.content) {
img { object-fit: cover; }
}
}

Similarly, we can allow & in a scoping context to refer to the scope root selector:

/* explicit scope with root */
@scope (.media) to (.content) {
& > img { object-fit: cover; }
}

This is much like using :scope, but they have slightly different matching behavior. The & selector represents an entire selector list, and could match nested .media elements – While the :scope selector represents a specific scoping root element in the DOM.

What happens when the root element matches the lower-boundary?

It’s possible that authors might use the same selector for both the root and lower-boundary of a scope (or use different selectors that match the same element):

a { color: rebeccapurple; }

@scope (.dark-theme) to ([class*='-theme']) {
a { color: plum; }
}

Since the lower boundary selector is scoped by the root selector, it is implicitly a descendant unless otherwise specified:

<!-- dark-theme scope root -->
<div class="dark-theme">
<a href="#">plum</a>

<!-- dark-theme scope boundary -->
<div class="light-theme">
<a href="#">rebeccapurple</a>
</div>

<!-- dark-theme scope boundary & nested scope root -->
<div class="dark-theme">
<a href="#">plum</a>
</div>
</div>

There aren’t any obvious use-case for single-element scopes, but they can be achieved with the explicit :scope selector:

@scope (.self) to (:scope *) { /* ... */ }

Where does scope fit in the cascade?

For a more detailed exploration of this, see: scope in the cascade.

The 2014 scope proposal

The original scope specification put scope above specificity in the cascade, and the layering was importance-relative:

For normal declarations the inner scope’s declarations override, but for ‘’!important’’ rules outer scope’s override.

That would mean first that scope takes precedence over specificity. By default, the more locally-scoped style always wins:

In this example from the outdated specification, a paragraph matched by both selectors would be green:

@scope aside {
p { color: green; }
}

aside#sidebar p { color: red; }

But the roles would reverse when !important is used, and the following example paragraph would be red:

@scope aside {
p { color: green !important; }
}

aside#sidebar p { color: red !important; }

Shadow-DOM encapsulation context

Shadow-DOM encapsulation context also comes above/before specificity in the cascade, with an importance-reversal. To quote the spec:

When comparing two declarations that are sourced from different encapsulation contexts, then for normal rules the declaration from the outer context wins, and for important rules the declaration from the inner context wins.

This is the opposite of the original scope proposal, and means:

…normal declarations belonging to an encapsulation context can set defaults that are easily overridden by the outer context, while important declarations belonging to an encapsulation context can enforce requirements that cannot be overridden by the outer context.

The case for less isolation, and weak proximity

We have intentionally gone in the other direction, making scope proximity less powerful than specificity in the cascade. There is clearly interest in both strong & weak approaches to scope, but we believe encapsulation context can be expanded and improved on for the high-isolation use-cases. Meanwhile low-isolation scope has not been addressed.

By placing scope proximity below/after specificity in the cascade, We are explicitly & intentionally allowing more global styles to flow through, interact with, and even override scoped styles:

@​scope (aside) {
p { color: green; }
}

aside#sidebar p { color: red; }
<aside id="sidebar">
<p>This is red</p>
</aside>

The primary use-case that we’re trying to address is one in which component-styles are “locked-in” to avoid cross-contamination, but global styles are used to “tie it all together” with consistent patterns like typography and branding. The desired behavior is to prevent scoped styles from leaking out, without getting in the way of global patterns that should flow through easily.

If we give scope proximity more weight than specificity, authors are left with very few tools to manage that relationship. By putting proximity below specificity, authors can manage it in several ways:

This in-but-not-out approach also matches the existing JS tools & CSS naming conventions that authors already use. Those tools add lower-boundaries, and a single attribute-selector of increased specificity – very easy to override from the global scope. We think this low-weight approach to scope is also backed up by…

Anecdotally, many CSS beginners surprised that the fallback for specificity is source-order rather than proximity. This proposal would allow authors to opt-into that expected proximity-over-source-order fallback behavior.

Meanwhile, encapsulation could be expanded for use in the light DOM, and proposal would continue to be distinct – covering a significantly different set of use-cases.

Alternative approaches to specificity

Existing tools achieve donut scope by appending a single attribute to each selector. If we wanted to match that behavior, we could give :scope the normal pseudo-class weight – even when implicitly added. Given the following code:

@scope (#media) to (.content) {
img { /* implied :scope ancestor */ }
:scope img { /* explicit :scope ancestor */ }
}

This approach would result in a specificity of [0, 1, 1] for each selector.

Sara Soueidan has also proposed giving @scope the selector-weight of an #ID. That would acknowledge the targeting weight of scopes, without making them override all specificity. While it’s an interesting idea, it seems less-intuitive & less flexible than the alternatives.

The original proposal was designed to match CSS Nesting when it comes to specificity. We can do that by applying the scope-root specificity to the overall specificity of each selector – as though de-sugared to :is(<scope-start>). The following examples would all have a specificity of [1, 0, 1]:

@scope (#footer, footer) {
a { /* :is(#footer, footer) a */ }
:scope a { /* :is(#footer, footer) a */ }
}

This matches the behavior of & in nested selectors, as well as ensuring the same specificity with or without the explicit use of :scope.

However, the group has resolved in favor of scope not having any impact on specificity. While specificity is often the way authors think about managing priority, scope is a different concern (like layers or shadow context) and there is no reason to muddy the water.

Spec History & Context

Besides the tooling that has developed, there are several current & former specs that are relevant here…

CSS Scoping

There is often pushback to the question of scope, since the initial specification was never implemented, and Shadow DOM was seen as a path forward. While the current editors draft is primarily concerned with Custom Elements & Shadow DOM, this spec initially contained a full set of scoping features that have since been removed:

A <style scoped> attribute, which would apply styles scoped to a particular DOM sub-tree. This had a few limitations:

The use-cases that necessitate that approach are now being handled by shadow encapsulation, which frees us up to consider different use-cases now.

The spec also included @scope blocks in CSS, which would help alleviate both issues. Scoping has two primary effects:

  1. The selector of the scoped style rule is restricted to match only elements within a subtree of the DOM
  2. The cascade prioritizes scoped rules over un-scoped ones, regardless of specificity
  3. Important declarations would flip the cascade order of scopes

Point 1 is limited by the need for lower scope boundaries, or “donut scope”.

Points 2 & 3 give scope significant power in the cascade – power that we now plan to provide through Cascade Layers. While there are instances where the semantics of layering, scoping, and containment might reasonably overlap – the combinations remain more flexible when each one has it’s own syntax.

In this proposal, scope is only given a minimal role in the cascade, and mostly acts as a protection from naming conflicts.

CSS Selectors - Level 4

CSS Cascade - Level 4

Stakeholder Feedback / Opposition

References & acknowledgements

Related/previous issues and discussions:

In addition to the open issue threads mentioned above, thanks for valuable feedback and advice from:

Change log

2023.04.13

2023.03.21

2021.08.24

2021.02.03

2021.01.29

2021.01.28

2021.01.27

2021.01.18