EthicsComputerCourses
Join training +91 90532 10052

@scope and Scoped Styles

CSS · Lesson 7 of 68 Beginner 8 min read Updated

Every CSS selector is global. A rule for .title styles every element with that class on the whole page, which is why teams invent long class names such as .product-card__title to avoid clashes. The @scope at-rule gives you another option: you say "these rules only apply inside this component", and short, simple selectors stay safe.

In this lesson you will write @scope rules, add a scope limit so styles stop at a nested component, learn how the browser decides between two scopes with scoping proximity, and see where @scope fits next to BEM, nesting and cascade layers.

The problem @scope solves

Imagine a shopping site with a product card and a seller box. Both have a heading with the class title:

CSS
.title { font-size: 1.25rem; color: #1e3a8a; }   /* meant for product cards */
.title { font-size: 0.9rem; color: #475569; }    /* meant for seller boxes */

The second rule wins everywhere, because both selectors have the same specificity and it comes later. The usual fixes have costs. Writing .card .title raises specificity, so the rule becomes harder to override later. Renaming everything with BEM works (see Organising CSS), but the names get long.

@scope keeps the short selector and limits where it can match.

Writing a @scope rule

The scope root goes in brackets after @scope. Selectors inside the block only match the root itself and elements inside it. The special pseudo-class :scope selects the root element.

HTMLTry it yourself
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>@scope basics</title>
  <style>
    body { font-family: system-ui, sans-serif; padding: 16px; display: grid; gap: 16px; }

    @scope (.card) {
      :scope { padding: 16px; border-radius: 12px; background: #eff6ff; }
      .title { margin: 0; font-size: 1.25rem; color: #1e3a8a; }
      .price { font-weight: 700; }
    }

    @scope (.seller) {
      :scope { padding: 12px; border: 1px solid #cbd5e1; border-radius: 8px; }
      .title { margin: 0; font-size: 0.9rem; color: #475569; text-transform: uppercase; }
    }
  </style>
</head>
<body>
  <article class="card">
    <h2 class="title">Steel water bottle, 1 litre</h2>
    <p class="price">₹499</p>
  </article>
  <aside class="seller">
    <h2 class="title">Sold by Meera Traders, Pune</h2>
  </aside>
  <h2 class="title">This title is outside both scopes, so neither rule styles it</h2>
</body>
</html>

Both components use .title, and each gets its own look. The last heading matches neither scope, so it keeps the browser defaults. Nothing leaked.

Scope limits: stopping at a nested component

Real components contain other components. A card might hold a slot where a different team places a review widget, and you do not want the card's p or .title rules to style the inside of that widget.

Add to (selector) after the root to set a scope limit. Elements inside the limit are outside the scope. Developers call this shape a "donut": the rules cover the ring and skip the hole.

HTMLTry it yourself
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Scope limits</title>
  <style>
    body { font-family: system-ui, sans-serif; padding: 16px; }

    @scope (.card) to (.slot) {
      :scope { padding: 16px; border-radius: 12px; background: #fef3c7; }
      p { color: #92400e; font-weight: 600; }
    }
    /* An ordinary rule, outside the scope */
    .slot { border: 2px dashed #d97706; padding: 8px; }
  </style>
</head>
<body>
  <article class="card">
    <p>Handmade Jaipur blue pottery mug, ₹650</p>
    <div class="slot">
      <p>Review widget: "Lovely colours, arrived safely in Kochi." (Ananya)</p>
    </div>
  </article>
</body>
</html>

The first paragraph is brown and bold. The paragraph inside .slot is untouched. By default the root is inclusive (the root itself can be styled with :scope) and the limit is exclusive: the limit element and everything inside it are outside the scope. That is why the dashed border is written as a normal rule after the scope. If you want the scope to style the limit element itself but not its contents, move the boundary one level down with to (.slot > *).

Scoping proximity: the nearest scope wins

Scopes can overlap. A classic case is a light section inside a dark section inside a light page. Without @scope, the rule that comes last in the file wins, whatever the nesting.

With @scope, the cascade has an extra step called scoping proximity: when two scoped rules conflict, the one whose scope root is fewer steps up the HTML tree wins.

HTMLTry it yourself
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Scoping proximity</title>
  <style>
    body { font-family: system-ui, sans-serif; padding: 16px; }
    section { padding: 16px; margin-top: 8px; border-radius: 10px; }

    @scope (.dark) {
      :scope { background: #1e293b; }
      p { color: #f8fafc; }
    }
    @scope (.light) {
      :scope { background: #f1f5f9; }
      p { color: #0f172a; }
    }
  </style>
</head>
<body>
  <section class="light">
    <p>Light section: dark text.</p>
    <section class="dark">
      <p>Dark section inside: light text, even though the .light rule comes later in the CSS.</p>
      <section class="light">
        <p>Light again: the nearest scope root is .light.</p>
      </section>
    </section>
  </section>
</body>
</html>

The middle paragraph is light text on a dark background. Both p rules have the same specificity, and .light comes later in the file, so the old cascade would have made it dark text. Proximity decides instead, because .dark is the closer root.

Proximity sits low in the cascade order. Origin and !important come first, then cascade layers, then specificity, then scoping proximity, and only then order of appearance. So a more specific selector still beats a closer scope.

Specificity inside @scope

The scope root does not add to the specificity of the selectors inside the block. This keeps scoped rules easy to override.

Selector inside @scope (.card)SpecificityWhy
p0-0-1The root adds nothing
.price0-1-0One class
:scope0-1-0:scope counts as a pseudo-class
:scope p0-1-1Pseudo-class plus type

You may also see & inside @scope. Its behaviour has differed between browser versions, so use :scope when you mean the root.

Inline scopes in a style element

If you write @scope without brackets inside a <style> element, the scope root is the parent of that <style> element. This is handy for a block of HTML that carries its own styles, such as an email-style banner or a widget in a content management system:

HTML
<div class="offer">
  <style>
    @scope {
      :scope { padding: 1rem; background: #dcfce7; }
      strong { color: #166534; }
    }
  </style>
  <p><strong>Monsoon sale:</strong> 20% off umbrellas in Kochi stores.</p>
</div>

The rules can only reach inside .offer, so a strong elsewhere on the page is safe.

When to use @scope

  • Use it for components with generic inner names (.title, .body, img) where BEM names feel heavy.
  • Use a scope limit when a component hosts content you do not control: a slot, a CMS area, a third-party widget.
  • Keep cascade layers for ordering whole groups of styles. Layers and scopes solve different problems and work well together.
  • CSS Nesting also groups rules under a component, but nested selectors add the parent's specificity and have no lower limit. @scope adds no specificity and can stop at a limit.

Common mistakes

  • Expecting full isolation. Inherited properties such as color and font-family still flow into the limit area. Set them again inside the nested component if needed.
  • Selecting outside the root. Selectors such as :scope + p try to reach a sibling of the root, which is outside the scope, so they match nothing.
  • Forgetting that specificity beats proximity. .light p written outside any scope beats a scoped p, because its specificity is higher.
  • Mixing up the brackets. @scope .card { } is invalid; the root needs brackets: @scope (.card) { }.

Key points

  • @scope (root) { ... } limits selectors to the root element and its descendants; :scope selects the root.
  • to (limit) stops the scope at nested elements: the limit element and its contents are left out.
  • When scoped rules conflict, the closer scope root wins, after layers and specificity but before source order.
  • Selectors inside @scope get no extra specificity from the root; :scope counts as one pseudo-class.
  • @scope without a prelude inside a <style> element scopes to that element's parent.
  • @scope is Baseline Newly available (2026); older browsers skip the whole block.

Exercise

A news site in Lucknow has an .article component whose paragraphs should be 1.125rem with a line height of 1.7. Inside articles, editors sometimes paste an .embed box (a cricket score widget) that must keep its own small text. Write a @scope rule that styles the article root (max width 40rem, centred), its paragraphs and its h2 headings, but does not style anything inside .embed. Then add a separate @scope (.embed) rule that gives the widget a border and 0.85rem text.

Show solution
HTML
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Scoped article</title>
  <style>
    body { font-family: system-ui, sans-serif; padding: 16px; }
    @scope (.article) to (.embed) {
      :scope { max-width: 40rem; margin-inline: auto; }
      p { font-size: 1.125rem; line-height: 1.7; }
      h2 { color: #7c2d12; }
    }
    @scope (.embed) {
      :scope { border: 1px solid #94a3b8; border-radius: 8px; padding: 8px 12px; }
      p, h2 { font-size: 0.85rem; margin: 4px 0; }
    }
  </style>
</head>
<body>
  <article class="article">
    <h2>Rain delays the Ranji match</h2>
    <p>Play resumed after lunch at the Ekana stadium.</p>
    <div class="embed">
      <h2>Live score</h2>
      <p>Uttar Pradesh 212/4 (61 overs)</p>
    </div>
  </article>
</body>
</html>

The article rules stop at .embed, so the widget's heading and paragraph only get the .embed scope's small sizes. The .embed box is the limit, so the article rules skip it too, and its border comes from its own scope.

Quick Quiz

1. What does :scope select inside @scope (.card) { ... }?

2. In @scope (.card) to (.slot), which elements do the rules match?

3. Two scoped rules with equal specificity and no layers conflict. Which wins?

4. What is the specificity of p inside @scope (#main .card)?

Learn CSS with a mentor

Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call