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
:rootMatches the document tree's root element.
:scopeMatches 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
:scope0-1-0
:scope > p0-1-1
:scope > .item0-2-0
:scope .item0-2-0
div:scope0-1-1
:scope > #title1-1-0
:where(:scope) > .item0-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 :scope when a selector must be evaluated relative to a specific reference element.
  • Combine :scope with > to select direct children.
  • Use element-based querySelector() and querySelectorAll() to keep queries focused on a container.
  • Do not add :scope unnecessarily when a simple descendant selector already provides the intended result.
  • Use :root for document-root styling and :scope for context-dependent scoping.
  • Use @scope when CSS rules need an explicit scope root or lower boundary.
  • Remember that :scope refers to the established scope root inside an @scope rule.
  • Use :where(:scope) when an explicit scope reference should not increase specificity.
  • Do not confuse :scope with the Shadow DOM :host pseudo-class.
  • Test @scope support separately from basic :scope support.

: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

Play in Editor

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.

Selectors Level 4 - The :scope Pseudo-Class

CSS Cascading and Inheritance Level 6 - Scoped Styles