Scope Proposal & Explainer

NOTE: This explainer may be out of date, since work is now happening on the official specification: CSS Cascade & Inheritance Module level 6.

Authors

Participate

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

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

Table of contents

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 automatically adding 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.

I 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.

I have not attempted to address that form of scope in my 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

Re-introducing the @scope rule

This would likely belong in the CSS Scoping Module.

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 }
}

I think that’s a good place to start.

In my mind, the first (“from”) clause may not need explicit labeling. It would accept a single (complex) selector (or selector list?):

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

In terms of selector-matching, this would be the same as .media-block img, but with slightly different cascade implications (see cascade section).

The second (“to”) clause would be optional, and accept a list of selectors that represent lower-boundary “slots” in the scope. The targeted lower-boundary elements are included in the scope, but their descendants are not:

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

Which would only match img and .content inside .media-blockbut not if there are intervening .content 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 .content is in-scope -->
<img src="..."><!-- this img is NOT -->
<div class="more content">...</div><!-- this .content is NOT -->
</div>
</div>

This approach keeps scoping confined to CSS (no need for a new HTML attribute), flexible (scopes can overlap as needed), and low-impact (global styles continue to work as expected). Existing tools would still be able to provide syntax sugar for single-file components – automatically generating the from/to clauses – but move the primary functionality into CSS.

Finally, we could allow @scope without any selector clauses, which would scope the styles to the parent of the stylesheet’s owner node (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 (existing) :scope pseudo-class

In most cases we can infer that the @scope root selector is prepended to all internal selectors with an ancestor/descendant relationship:

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

There is even an existing Reference Element Pseudo-class (:scope selector), which we can use to represent that behavior. It is currently supported in JS APIs to refer to the base element of e.g. element.querySelector(). The following blocks would be identical:

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

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

Authors can also use :scope to express more complex relationships between a scoped selector and the scope-root. For example, adding an explicit combinator:

@scope (.media) {
:scope > img { object-fit: cover; }
}

Or matching based on additional context outside the scope:

@scope (.media) {
.sidebar :scope img { object-fit: cover; }
}

Or styling the scope-root directly:

@scope (.media) {
:scope { display: grid; }
}

This is especially useful if we want to target a nested instance of the scope root selector:

@scope (.media) {
/* select only the scope-root .media */
:scope { display: grid; }

/* select nested .media inside the scope */
.media { background: gray; }
:scope .media { background: gray; }
}

Since there is no way to have the root element inside itself, we would not support nested instances of :scope itself:

@scope (.media) {
/* no match */
:scope :scope { background: gray; }
:scope + :scope { background: gray; }
}

Note: There has also been some discussion of using the nesting module & nested-selector for this. But & acts as an alias for duplicating a selector list, while :scope refers to a specific element that is acting as the root of a given context. Given the likelihood that nesting and scope will be used together, it seemed important that these ideas remain distinct.

A new donut selector?

Lea Verou has suggested that it might be useful for authors to access “donut-matching” as a distinct feature, apart from the cascade rules of scoping.

That could be done with a pseudo-selector. The name would need bike-shedding, but let’s call it in() for now. This new selector would need to describe the entire donut, taking two arguments: a root selector, and lower-boundary selector. Since spaces and commas exist inside selectors, we could borrow the slash as a delimiter. Imagine a syntax like:

/* :in(<selector> / <selector>) */
:in(.root .selector / .lower, .boundaries)

The following selectors would match the same elements:

@scope (.media) to (.content) {
img { border: red; }
}

img:in(.media / .content) { border: red; }

In practice, there would be very little difference between this selector and a scope rule – since scope proximity is weighted below specificity in the cascade. It’s not clear to me how many use-cases would want donut-selection while opting out from scope proximity.

I explore this idea more as part of my syntax comparison.

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.

At first that seemed potentially confusing, but after many conversations, I think it may be the most expected behavior.

Specificity

There are several ways we could handle specificity around scoping. Assuming we support both @scope/:scope and a selector like :in(), we would likely want to match the specificity of all these selectors:

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

img:in(#media / .content) { /* donut selector */ }

Based on feedback so far, I lean towards applying the scope-root specificity to the overall specificity of each selector. All the examples above would get a specificity of [1, 0, 1].

Some alternative approaches are discussed below.

Scope Proximity

This would likely belong in CSS Cascading & Inheritance.

The syntax above solves the issue of naming-conflicts, with lower-boundaries/ownership. But the issue of scope proximity requires changes in the Cascade. My sense is that scope proximity should override source order, but otherwise cascade layers & specificity should take precedence.

Given the same origin & importance, layering, and specificity – inner “more proximate” scope would take precedence over outer/global “less proximate” scopes:

@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>

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>

However, in this proposal specificity can 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. That is useful in certain cases, but many authors desire a lighter-touch scope – allowing global styles to easily flow through scoped components, while preventing scoped styles from leaking out.

I 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 (form) {
.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:

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

/* only the outer image */
: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[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 { /* ... */ }
}

/* 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

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

Shadow-DOM is simply 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:

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>

Would we need special handling around the shadow-DOM?

This should have no impact on existing shadow DOM behavior. Scoped styles can be used either in light or shadow DOM. All scopes & scoped styles continue to respect the shadow boundary, the same as 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?

Can tree-abiding pseudo elements be scope roots?

This issue was also raised by Rune.

Is this allowed?

@scope (div::before) {
:scope { content: "xxx" }
}

I don’t know of any use-case for it, and would defer to input from implementors.

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 *)");

But that is only possible one element at a time, and JS does not provide any way to fetch “donuts” initially.

The proposed :in() pseudo-class might allow authors to target elements that fall within a donut, but not to return a tree fragment of the donut itself:

document.querySelectorAll("<element>:in(<root> / <boundary>)");

In order to return a donut tree fragment, I think we would need a second “exclusions” parameter on methods like querySelector() and querySelectorAll():

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

But I am not an expert on these JS issues, and would welcome more input.

How does scope interact with the nesting module?

This proposal has some overlap with the CSS Nesting Module (currently an Editor’s Draft).

With the nesting syntax, we could allow the @scope rule to be nested inside an existing selector block, much like @nest, and establish scope-root based on the outer selector. In this case, the scope-root selector must be nest-containing (have & somewhere in it), with an implied value of &. These three code-blocks 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; }
}
}

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

However, the goal of nesting is to clean up document structure, and make it more readable – primarily a syntax issue. Scope has a much more complicated set of goals, around limited selector-matching and namespacing. There is also a clear difference between the meaning of & (which represents an entire selector list) and :scope (which represents a specific scoping element in the DOM).

While it might be a mistake to make one rely on the other – they clearly have some overlap. Both help to describe the relationship between parent & child selectors.

Can scope rules be nested in other scope rules?

I can imagine use-cases for nesting scope rules, though I’m not aware of any tools that currently do this. I believe we could describe a behavior that works as expected. Let’s take, for example:

@scope (.media) to (.content) {
@scope (figure) to (figcontent) {
img { border: red; }
}
}

I would expect that to be “flattened” into a single scope. We can express that using the proposed :in() donut selector as part of the scope root:

@scope (figure:in(.media / .content)) to (figcontent) {
img { border: red; }
}

The figure itself would be considered the scope-root.

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; }
}

I would expect the lower boundary to only match descendants within each instance of the scope:

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

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

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

Rather than applying to a single element as both root and lower-bounds:

<!-- outer scope root & self-boundary -->
<div class="dark-theme">
<a href="#">rebeccapurple</a>
</div>

If there is a use-case for single-element scopes, we could allow that using a more explicitly self-referential syntax:

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

Which would only match selectors targeting the scope-root itself, such as:

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

Where does scope fit in the cascade?

For a more detailed exploration of this, see my notes on 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

I 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 I 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, I am 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 I’m 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. I think this low-weight approach to scope is also backed up by…

Anecdotally, I hear 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/:in() the normal pseudo-class weight. Given the following code:

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

img:in(#media / .content) { /* donut selector */ }

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

Another option would be to remove scope from specificity entirely – for a final weight of [0, 0, 1]. That would “simplify” the impact of scope on the cascade, but at the expense of some clarity about the relationships.

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. I can see the thought behind it, but it seems less-intuitive & less flexible than the alternatives.

Can we improve on the syntax?

The syntax could use some discussion, especially around the proper label for lower boundaries. There has been discussion of other keywords like until, as well as a function syntax (eg to(<selector>)). I’d also consider rephrasing to label these as “slots” rather than “end-points”:

@scope root(.media-block) slots(.content) { /* ... */ }

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 – I think all three features are better off with their own syntax.

In my 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

2021.08.24

2021.02.03

2021.01.29

2021.01.28

2021.01.27

2021.01.18