CSS :scope Pseudo-Class
The CSS :scope pseudo-class represents the current scoping root used to evaluate a selector. It is especially useful with JavaScript methods such as querySelector() and querySelectorAll(), where it allows elements to be selected relative to a specific container. It can also be used in CSS scoping rules to reference the established scope root.
:scope Syntax
:scope {
property: value;
}
The :scope pseudo-class represents the scoping root in the current selector-matching context. Its meaning depends on where the selector is evaluated.
Stylesheet Example
:scope {
background-color: #f2f2f2;
}
In an ordinary document stylesheet without an explicit scoping context, :scope matches the document's root element.
JavaScript Example
const container = document.querySelector(".container");
const heading = container.querySelector(":scope > h2");
Finds an <h2> that is a direct child of the selected container.
Scoped CSS Example
@scope (.card) {
:scope {
border: 2px solid #006699;
}
}
Inside this @scope rule, :scope refers to each matching .card scope root.
No Arguments
The :scope pseudo-class does not accept arguments and is written without parentheses.
How :scope Works
The :scope pseudo-class identifies the reference point used when evaluating a selector. That reference point may be the document root, an element used for a JavaScript query, or a root established by an @scope rule.
Document Context
:scope {
font-family: Arial, sans-serif;
}
In a normal document stylesheet, the selector matches the document root.
Element Query Context
const section = document.getElementById("example");
const items = section.querySelectorAll(":scope > p");
The selector finds paragraphs that are direct children of the section.
Scoped Stylesheet Context
@scope (.panel) {
:scope > h2 {
color: #006699;
}
}
The rule styles headings that are direct children of each .panel scope root.
Context Determines Matching
The same :scope selector can refer to different elements depending on the evaluation context.
Not a Permanent Element State
Unlike pseudo-classes such as :hover or :checked, :scope does not describe a temporary interaction state. It identifies a selector's scoping reference.
The Scoping Root
A scoping root is the element or reference point relative to which a selector is evaluated.
HTML Example
<section id="tutorial-section">
<h2>CSS Selectors</h2>
<p>Introduction</p>
<div>
<p>Nested paragraph</p>
</div>
</section>
JavaScript Example
const section = document.getElementById("tutorial-section");
const paragraphs = section.querySelectorAll(":scope > p");
Here, the section acts as the scoping root. Only its direct paragraph child is selected, not the paragraph nested inside the div.
Relative Selection
The :scope pseudo-class makes the relationship between the reference element and selected descendants explicit.
Different Containers
The same selector can be reused with different containers. Each query evaluates :scope relative to its own reference element.
Document vs. Element
When querying a document, :scope represents the document element. When querying an element, it represents that element.
:scope vs. :root
The :scope and :root pseudo-classes can match the same element in a normal document stylesheet, but they serve different purposes.
Comparison
| Pseudo-Class | Matching Behavior |
|---|---|
:root | Matches the document tree's root element. |
:scope | Matches the scoping root determined by the current selector context. |
Document Stylesheet
:root {
--primary-color: #006699;
}
:scope {
color: #333;
}
In a normal HTML document stylesheet, both selectors match the <html> element.
Element Query
const panel = document.querySelector(".panel");
const directItems = panel.querySelectorAll(":scope > .item");
In this query, :scope refers to the panel rather than the document's <html> element.
When to Use Each
Use :root for document-root styling and global custom properties. Use :scope when a selector must refer to its current scoping context.
Using querySelector()
The JavaScript querySelector() method returns the first element matching a CSS selector. When called on an element, :scope can identify that element as the reference point.
HTML Example
<div id="example-card">
<h2>Card Heading</h2>
<p>Card description</p>
</div>
JavaScript Example
const card = document.getElementById("example-card");
const heading = card.querySelector(":scope > h2");
console.log(heading.textContent);
The query selects the heading directly inside the card.
Why Use :scope?
It makes the intended relationship explicit and helps avoid matching a deeper descendant when only a direct child should be selected.
Without :scope
const heading = card.querySelector("h2");
This query returns the first matching heading anywhere within the card, including a heading nested inside another descendant.
With :scope
const heading = card.querySelector(":scope > h2");
This query only matches an <h2> that is an immediate child of the card.
Using querySelectorAll()
The querySelectorAll() method returns a static collection of elements matching a selector. Combining it with :scope is useful for selecting a specific container's children.
HTML Example
<ul id="main-list">
<li>HTML</li>
<li>CSS
<ul>
<li>Selectors</li>
<li>Properties</li>
</ul>
</li>
<li>JavaScript</li>
</ul>
JavaScript Example
const list = document.getElementById("main-list");
const items = list.querySelectorAll(":scope > li");
console.log(items.length);
The query returns three direct list items. It excludes the two items inside the nested list.
Without Direct-Child Restriction
const allItems = list.querySelectorAll("li");
This query returns all five list items because it includes nested descendants.
Static Collection
The returned NodeList does not automatically update when matching elements are added or removed. Run the query again to obtain an updated collection.
Reusable Selector
The selector :scope > li can be used with different lists without changing the selector text.
Selecting Direct Children
One of the most useful patterns for :scope is combining it with the child combinator >.
Basic Pattern
container.querySelectorAll(":scope > .item");
Selects elements with the class item that are direct children of the container.
Nested Structure
<div id="container">
<div class="item">Direct Item 1</div>
<div class="item">Direct Item 2</div>
<div class="wrapper">
<div class="item">Nested Item</div>
</div>
</div>
Direct-Child Query
const container = document.getElementById("container");
const items = container.querySelectorAll(":scope > .item");
Matches Direct Item 1 and Direct Item 2, but not Nested Item.
Descendant Query
const items = container.querySelectorAll(".item");
Matches all three items because the descendant relationship is not restricted to direct children.
Why the Difference Matters
Direct-child selection is useful when components contain nested elements with the same class names.
Nested Elements and Components
Reusable components often contain other components. The :scope pseudo-class helps JavaScript distinguish a container's immediate children from elements nested deeper in the structure.
HTML Example
<div class="menu" id="main-menu">
<div class="menu-item">Home</div>
<div class="menu-item">
Tutorials
<div class="submenu">
<div class="menu-item">HTML</div>
<div class="menu-item">CSS</div>
</div>
</div>
</div>
JavaScript Example
const menu = document.getElementById("main-menu");
const topLevelItems = menu.querySelectorAll(":scope > .menu-item");
Selects only the two top-level menu items.
Nested Items
const nestedItems = menu.querySelectorAll(".submenu .menu-item");
Selects the menu items inside the submenu.
Component Isolation
Using a specific reference element reduces the chance of accidentally selecting similarly named elements elsewhere in the document.
Important Distinction
Element-based queries are already limited to descendants of the reference element. The benefit of :scope is making relationships to that element explicit, especially when using combinators.
The @scope At-Rule
The CSS @scope at-rule establishes a scope root for a group of style rules. Inside the scoped rule, :scope can explicitly select that root.
Basic @scope Example
@scope (.card) {
:scope {
border: 2px solid #006699;
}
h2 {
color: #006699;
}
}
The :scope selector matches the card root. The heading rule applies to matching headings within the scope.
Direct Children
@scope (.card) {
:scope > h2 {
margin-top: 0;
}
}
Styles headings that are direct children of the scoped card.
Implicit Scope Root
@scope (.card) {
p {
line-height: 1.6;
}
}
Selectors inside @scope are evaluated within the established scope. The :scope pseudo-class is useful when a rule needs to reference the root explicitly.
Scope Root vs. Document Root
Inside this rule, :scope refers to the matching card rather than the document's <html> element.
Browser Compatibility
The @scope at-rule is newer than the basic :scope pseudo-class. Check compatibility separately before relying on it in production stylesheets.
Scope Limits
The @scope at-rule can define a lower boundary, called a scope limit. This allows styles to apply within a container while excluding a nested region.
Scope Root and Limit
@scope (.article) to (.example-box) {
p {
color: #333;
}
}
Styles paragraphs inside an article's scope while excluding paragraphs within matching example boxes and their descendants.
HTML Example
<article class="article">
<p>Article introduction.</p>
<div class="example-box">
<p>Example paragraph.</p>
</div>
<p>Article conclusion.</p>
</article>
Matching Behavior
The introduction and conclusion are within the scope. The example paragraph is inside the excluded region.
Using :scope
@scope (.article) to (.example-box) {
:scope {
border-left: 3px solid #006699;
}
}
The :scope selector refers to the article root, which is inside its own scope and is not excluded by the limit.
Scope Limits Are Not JavaScript Query Limits
The lower boundary defined by @scope affects CSS rule matching. It does not automatically change the behavior of JavaScript querySelectorAll() calls.
Shadow DOM
Shadow DOM creates a separate tree for a component's internal elements. Selector scoping and JavaScript queries behave differently across shadow boundaries.
Shadow Root Query
const host = document.querySelector("my-component");
const shadow = host.shadowRoot;
const items = shadow.querySelectorAll(".item");
When the component has an open shadow root, this query selects matching elements inside that shadow tree.
Shadow Host
The :host pseudo-class is used in shadow-tree stylesheets to select the element hosting the shadow tree.
Scope and Host Are Different
The :scope pseudo-class identifies a scoping root in the current selector context. It is not interchangeable with :host, which specifically refers to a shadow host.
Shadow Boundaries
Ordinary document selectors and element queries do not automatically traverse into a component's shadow tree.
Component Styling
Use :host, ::part(), custom properties, and other Shadow DOM styling mechanisms when working with encapsulated components.
:scope Specificity
The :scope pseudo-class contributes a specificity of 0-1-0, like other standard pseudo-classes.
Specificity Examples
| Selector | Specificity |
|---|---|
:scope | 0-1-0 |
:scope > p | 0-1-1 |
:scope > .item | 0-2-0 |
:scope .item | 0-2-0 |
div:scope | 0-1-1 |
:scope > #title | 1-1-0 |
:where(:scope) > .item | 0-1-0 |
Specificity Example
@scope (.card) {
:scope > h2 {
color: #006699;
}
}
The explicit selector :scope > h2 has a specificity of 0-1-1. The @scope root selector does not add its own specificity to the enclosed rule.
Implicit Scope
Selectors written inside @scope are scoped without automatically adding the specificity of the scope root selector.
Using :where()
@scope (.card) {
:where(:scope) > h2 {
color: #006699;
}
}
Wrapping :scope in :where() removes its specificity contribution. The resulting selector has a specificity of 0-0-1.
Practical Uses
The :scope pseudo-class is useful for selecting elements relative to a specific container and for referencing scope roots in CSS.
Direct List Items
const items = list.querySelectorAll(":scope > li");
Selects direct list items without including nested list items.
Card Headings
const heading = card.querySelector(":scope > h2");
Selects a heading directly inside a card.
Form Groups
const groups = form.querySelectorAll(":scope > .form-group");
Selects only top-level form groups.
Navigation Items
const links = nav.querySelectorAll(":scope > a");
Selects links that are direct children of a navigation container.
Scoped Component Root
@scope (.component) {
:scope {
border: 1px solid #ccc;
}
}
Styles each matching component root inside a scoped CSS rule.
Scoped Headings
@scope (.component) {
:scope > h2 {
color: #006699;
}
}
Styles headings that are immediate children of a component root.
Limitations
The :scope pseudo-class is context-dependent. Its behavior cannot be understood without considering where the selector is evaluated.
Not Always the Document Root
In element-based JavaScript queries or scoped CSS rules, :scope can refer to an element other than <html>.
Not a Global Container Selector
It does not automatically select every container or parent element in a document.
Element Queries Are Already Restricted
Calling container.querySelectorAll(".item") already returns matching descendants of the container. Use :scope when you need to express a relationship to that container.
Complex Selector Behavior
In element-based queries, complex selectors can be evaluated with ancestors outside the reference element participating in matching. Use :scope to anchor a selector when necessary.
Does Not Cross Shadow Boundaries
Using :scope does not make ordinary selectors traverse into closed or separate shadow trees.
Different from @scope
The :scope pseudo-class identifies a scoping root. The @scope at-rule establishes a scope for CSS rules.
Browser Support Differences
The :scope pseudo-class and @scope at-rule have different compatibility histories. Check them separately.
Specificity
An explicit :scope adds pseudo-class specificity. Use :where(:scope) if zero specificity is desirable.
Best Practices
- Use
:scopewhen a selector must be evaluated relative to a specific reference element. - Combine
:scopewith>to select direct children. - Use element-based
querySelector()andquerySelectorAll()to keep queries focused on a container. - Do not add
:scopeunnecessarily when a simple descendant selector already provides the intended result. - Use
:rootfor document-root styling and:scopefor context-dependent scoping. - Use
@scopewhen CSS rules need an explicit scope root or lower boundary. - Remember that
:scoperefers to the established scope root inside an@scoperule. - Use
:where(:scope)when an explicit scope reference should not increase specificity. - Do not confuse
:scopewith the Shadow DOM:hostpseudo-class. - Test
@scopesupport separately from basic:scopesupport.
:scope Example
This example demonstrates the difference between selecting all descendant list items and selecting only direct children with :scope > li. Click Highlight Direct Items to highlight the top-level list items without highlighting nested items.
HTML
<div class="scope-demo">
<ul id="scope-list">
<li>HTML</li>
<li>CSS
<ul>
<li>Selectors</li>
<li>Properties</li>
</ul>
</li>
<li>JavaScript</li>
</ul>
<button type="button" id="highlight-direct">Highlight Direct Items</button>
<button type="button" id="highlight-all">Highlight All Items</button>
<button type="button" id="reset-highlight">Reset</button>
</div>
CSS
.scope-demo ul {
padding-left: 24px;
}
.scope-demo li {
margin: 5px 0;
padding: 5px;
}
.scope-demo .highlight {
background-color: #eaf6fc;
outline: 2px solid #006699;
}
.scope-demo button {
margin: 5px 5px 5px 0;
padding: 8px 10px;
cursor: pointer;
}
JavaScript
const list = document.getElementById("scope-list");
const clearHighlights = () => {
list.querySelectorAll("li").forEach(item => {
item.classList.remove("highlight");
});
};
document.getElementById("highlight-direct").addEventListener("click", () => {
clearHighlights();
list.querySelectorAll(":scope > li").forEach(item => {
item.classList.add("highlight");
});
});
document.getElementById("highlight-all").addEventListener("click", () => {
clearHighlights();
list.querySelectorAll("li").forEach(item => {
item.classList.add("highlight");
});
});
document.getElementById("reset-highlight").addEventListener("click", clearHighlights);
Browser Result
Note: Highlight Direct Items selects three top-level list items. Highlight All Items selects all five list items, including nested items. Reset removes the highlighting. The example uses :scope to demonstrate direct-child selection in JavaScript.
Browser Support
Baseline: Widely available. The :scope pseudo-class is supported in modern versions of Chrome, Edge, Firefox, Safari, and other major browsers. Its use with JavaScript selector methods is broadly supported.
The CSS @scope at-rule is a separate, newer feature. Browser compatibility for @scope should be checked independently.
For current compatibility information, visit Can I Use? and search for :scope or CSS @scope.
Specifications
The :scope pseudo-class is defined in CSS Selectors Level 4. Its use with scoped CSS rules is also described in CSS Cascading and Inheritance Level 6.
