8. Scope

A normal selector knows how to say where a style starts applying, but not where it ends. You write .card p and that color falls on every paragraph hanging off the card, wherever they are, no matter how deep they have sunk. What if you want to paint only one part of the tree and leave the rest alone?

That is what @scope is for. It lets you say "these rules only apply in here", fencing off a region of the document with a ceiling and, if you want, a floor as well.

The basic form

The minimal version is this:

@scope (.card) {
    p {
        color: #444;
    }
}

Translated: the paragraphs inside a .card are painted gray. So far the same as a plain old .card p { color: #444; }.

<div class="card">
    <p>I am inside the card, so I come out gray.</p>
</div>

<p>I am outside, this does not touch me.</p>

I am inside the card, so I come out gray.

I am outside, this does not touch me.

Less specificity, easier to override

There is a small but handy advantage. The specificity of the rule is that of the inner selector (p), not the sum of the outer selector plus the inner one. With @scope the .card in the header does not add weight, so overriding that style later costs you less:

@scope (.card) {
    p {
        color: #444;
    }
}

/* A single class is enough to win, no need to fight specificity */
.highlight {
    color: crimson;
}

The donut: adding a floor as well

Here is the interesting bit. On top of the ceiling you can mark a lower boundary with the keyword to:

@scope (.card) to (.card__footer) {
    p {
        color: #444;
    }
}

Now the rules start at .card and stop when they reach .card__footer. Look at it in the tree:

.card              ← starts
 ├── <p>           ✅ gray
 ├── <p>           ✅ gray
 │
 └── .card__footer ← stops
      ├── <p>      ❌ nothing
      └── <p>      ❌ nothing

It is a donut: there is dough all around and a hole in the middle. That is why it is called donut scope, and that is the part that really matters. A normal selector only knows how to say where it starts. @scope also knows how to say where it ends.

<div class="card">
    <p>Card body, in gray.</p>
    <p>Another body line, also in gray.</p>
    <div class="card__footer">
        <p>The footer sits outside the hole, it does not change.</p>
    </div>
</div>

Card body, in gray.

Another body line, also in gray.

This solves a classic pain. Think of a content block with nested cards, or a text editor that renders third-party HTML: you want to style the content, but without it leaking into the components living inside. The floor of the donut is exactly that.

:scope, the root element

Inside the block, the keyword :scope points to the very element that opens the scope, that is, the .card in the header. It is useful for styling the root in addition to what is inside:

@scope (.card) {
    :scope {
        border: 1px solid #ccc;
        border-radius: .5rem;
    }

    p {
        margin: 0;
    }
}

You can also use & to refer to that root when building selectors, just like in normal nesting.

Proximity breaks the tie

This is the trick no classic selector has. When two rules with the same specificity try to paint the same element, @scope gives the win to the one whose root is closer in the tree. It is called scope proximity.

Imagine a light theme and a dark theme nested inside each other:

@scope (.light) {
    p {
        color: #222;
    }
}

@scope (.dark) {
    p {
        color: #eee;
    }
}
<div class="light">
    <p>Text on a light background.</p>
    <div class="dark">
        <p>Here the .dark wins, since it is the nearest root.</p>
    </div>
</div>

Text on a light background.

Here the .dark wins, since it is the nearest root.

The inner paragraph matches both rules, because it lives inside .light and .dark at the same time. Both have the same specificity (a plain p), so the tie is decided by proximity: .dark wins for being closer. Without @scope you would have to reach for more convoluted selectors to achieve the same thing.

Do not replace all your rules with @scope. Its strong suit is the floor of the donut: fencing off a region without the style leaking into the components inside. For everything else, a normal selector is still shorter and clearer.

Activity 1

Create a .notice with several paragraphs inside. Using @scope, paint red only the paragraphs that are inside the notice and check that a paragraph placed outside does not change color.

Activity 2

Build a .card with a body and a .card__footer. Apply a donut scope with @scope (.card) to (.card__footer) to color the body and verify that the paragraphs in the footer are left without any style.

Building SPAs with Django and HTML Over the Wire: Learn to build real-time single page applications with Python

Building SPAs with Django and HTML Over the Wire: Learn to build real-time single page applications with Python

The HTML over WebSockets approach simplifies single-page application (SPA) development and lets you bypass learning a JavaScript rendering framework such as React, Vue, or Angular, moving the logic to Python. This web application development book provides you with all the Django tools you need to simplify your developments with real-time results.

Buy the book

Help me keep writing

Every coffee gives me a push toward the next article.

Comments

There are no comments yet.