Bilkeesu Babangida
Posted on September 29, 2024
Introduction
One of the key problems developers face with CSS is styles leaking out to components they should not apply to. Developers have relied on naming conventions like the BEM (Block Element Modifier) methodology. This new way of scoping helps us overcome most of the issues we have with the cascade. This article will teach you how to be specific with your styles using the @scope at-rule. This will save you from encountering specificity issues, style conflicts, and naming conflicts.
Definition of Scoping
Scoping offers a way to select HTML elements only within a region of the DOM. Scope helps you target specific parts of your HTML. This gives you more control over your CSS styles.
The @scope
at-rule opens up a scope block, to which we specify a scoping root in parenthesis. This creates a scoping block for the child elements of the specified root.
Below is the basic syntax:
@scope (root) {
element {
style
}
}
Any CSS we declare inside this scope will only apply to elements selected within the scope.
Why You Should Scope Your CSS
Scoping stops your styles from leaking out of one component and affecting other components. To some extent, scoping provides a way of ‘encapsulating’ CSS. Your selectors will only apply to the specific elements you want.
Without scope, you need to add a class to every element and also be very specific with the class names. This could lead to style and naming conflicts, making large projects difficult to maintain.
Let's take a look at the code below:
p {
color: red;
}
This p
selector will apply to all the p
elements in your code. You might want different styles for some paragraphs. Of course, you can assign a unique class name to each element you want to style differently. However, coming up with different class names can be tedious. It can result in name clashes and specificity issues as your project scales. Normally, we use specificity and order of appearance to style such exceptions, but this leads to style conflicts in some cases.
Scoping eliminates the need to add a class name to every element. You can use simple element selectors inside a scope block. You don’t have to worry about naming conventions or your styles applying to areas you don’t want.
How Does @scope Work?
Let’s take a look at how @scope
works in practice:
<div class="tag">
<p>Hello world</p>
</div>
<div class="card">
<p>Hello everyone</p>
</div>
In this HTML, we have two div
elements with the class names .tag
and .card
. Both div
elements have p
nested within them.
Copy this code snippet into your CSS:
@scope (.tag) {
p {
background: red;
}
}
In the code above, the p
selector will only apply to the paragraph inside the .tag
root.
Here's the output of the code:
How to Use @scope
There are various ways you can use the @scope rule to target certain elements you want. Let's explore what we can do with @scope.
Targeting Single Elements
You can target one root as we have seen in the previous example.
Let’s look at another example:
<div class="jane">
<p>my name is jane</p>
</div>
In this code, we have a div
with the class .jane
and a paragraph nested inside it.
@scope (.jane) {
p {
color: grey;
}
}
The code above will only apply to p
elements within the scoping root .jane
.
Targeting Multiple Elements
You can scope two roots at the same time. Let’s say you have two or more components where you want to apply the same styles to their nested elements. You can put them in one scope instead of having a separate scope for each component.
Let's see how this works:
<div class="mike">
<p>My name is Mike</p>
</div>
<div class="jane">
<p>My name is Jane</p>
</div>
This code consists of two div
elements with the class names .mike
and .jane
respectively. Each div
has a p
element nested inside of it.
The CSS code below will scope both the .mike
and .jane
components at the same time.
@scope (.mike, .jane) {
p {
color: grey;
}
}
This style will apply to the p
elements inside both .mike
and .jane
roots.
Setting a Scope Limit
@scope
allows you to define where a scope starts and ends. In the cascade, every style you apply to a parent will automatically go down to all the child elements of that parent. With scope, you can stop the cascade at a specific point. This is referred to as donut scope.
To create a donut scope, you define the upper and lower boundaries of your scope. This will create a hole in the scope, where styles do not apply.
Let’s see an example of how donut scope works:
<div class="birthday-card">
<h1>hello mary!</h1>
<p>have a nice birthday</p>
<div class="gift-card">
<h1>here's your gift</h1>
<p>Have a nice day</p>
</div>
</div>
Here, we have a parent div
with the class .birthday-card
. Inside the parent div
, there's child div
with the class .gift-card
.
The CSS code below will only apply to paragraphs within the upper boundary of the scope. Paragraphs within the lower boundary are considered out of scope.
@scope (.birthday-card) to (.gift-card) {
p {
border: 2px solid red;
}
}
Here, the .birthday-card
div
is the upper boundary of the scope, and the .gift-card
div
is the lower boundary of the scope (scope limit). This scope starts at .birthday-card
and stops at .gift-card
.
Here's the output:
Solving Specificity Issues With @scope
In the cascade, styles are applied based on specificity and order of appearance. If two declarations with the same specificity are targeting one element, the declaration that comes last will override all other declarations that come before it. This can cause a lot of style conflicts in most cases.
Let’s see an example to demonstrate this:
<div class="red-color">
<a href="#">I am a red link</a>
</div>
<div class="yellow-color">
<a href="#">I am a yellow link</a>
<div class="red-color">
<a href="#"> I am a red link</a>
</div>
</div>
The code above has a .red-color
div
and a .yellow-color
div
with another .red-color
div
nested inside of it.
.red-color a {
color: red;
border: 3px solid red;
}
.yellow-color a {
color: yellow;
border: 3px solid yellow;
}
As you can see above, the .yellow-color
class overrides the .red-color
class. The link color is yellow instead of red. Both selectors have the same specificity, but the .yellow-color
class won based on the order of appearance. The best way to solve this issue is to use scope proximity.
Here's the output:
Scope Proximity
Scope allows us to override styles based on the proximity of the scoping root to the target element. In scope, the order of things doesn't matter; It’s all about proximity rather than the order of appearance.
If elements have the same specificity, then scope proximity comes into play. If they have the same scope proximity, then the order of appearance will be applied. However, an element with a higher specificity will still override proximity.
When two scope declarations are targeting the same element, the cascade will give priority to the scoping root that is the closest parent of the target element.
We’ll continue working on our previous example to see how we can use scope proximity to solve the issue we had.
Include this code in your CSS:
@scope (.red-color) {
a {
color: red;
border: 3px solid red;
}
}
@scope (.yellow-color) {
a {
color: yellow;
border: 3px solid yellow;
}
}
The style here will apply based on scope proximity. The .red-color
class is the closest parent to the red link. This means that the .red-color
class has a stronger scope proximity, hence it gets more priority in this case.
Here's the output:
Let’s take a look at another example:
<div class="lightblue">
<div class="lightpink">
<button>CLICK ME</button>
</div>
</div>
In this HTML, we have a parent div
with the class name .lightblue
. Inside this parent, we have a .lightpink
div
with a button
nested within.
Add this code to your CSS:
@scope (.lightblue) {
button {
background: lightblue;
}
}
@scope (.lightpink) {
button {
background: lightpink;
}
}
The .lightpink
class will be applied here because it is the closest parent of the button.
Here's the output:
What will happen if we change the parent to .lightblue
? Well, let’s take a look:
<div class="lightpink">
<div class="lightblue">
<button>CLICK ME</button>
</div>
</div>
Here, we changed the parent div
to .lightpink
. The .lightblue
div
is now the child div
.
Here's the output:
The button is now light blue because the .lightblue
class has a stronger scope proximity here.
The :scope Pseudo-class
The :scope
selector targets the scoping root element. It has the same specificity as other pseudo-classes, which is 0,1,0.
<div class="flex-container">
<div class="flex-item">
<p>Hello World</p>
</div>
<div class="flex-item">
<p>Hello Universe</p>
</div>
</div>
This HTML has a parent div
with the class name .flex-container
. It contains two child div
with the class .flex-item
.
@scope (.flex-container) {
:scope {
max-width:250px;
display: flex;
gap:20px;
border: 3px solid red;
}
p {
color: yellow;
}
}
The :scope
selector will style the root element and the p
will style the paragraphs within the root.
Here's the output:
You can also use the &
to target the root. The &
is normally used in nesting, but it also works in scope.
@scope (.flex-container) {
& {
max-width:250px;
display: flex;
gap:20px;
border: 3px solid red;
}
p {
color: yellow;
}
}
This will give the same results as using :scope
. Although both :scope
and &
target the root, they differ in selector matching and specificity.
- The
:scope
selector has the same specificity as other pseudo-classes, while the specificity of&
changes according to the root element. - The
:scope
selector only matches the root of a scope, while&
matches the selector of the root.
Note that elements within a scope do not inherit the specificity of their scoping root. Element selectors such as p
, img
etc. will have their normal specificity of 0,0,1.
Applying Inline Styles With @scope
You can add @scope
directly into your HTML code by embedding an inline style
tag. You don’t need to specify a root for your scope when using inline styles. The scope will automatically apply to the parent of the style
tag.
<div class="inline-scope">
<a href="#">I am a scoped link</a>
<p>I am a scoped paragraph</p>
<style>
@scope {
:scope {
background: grey;
}
a:hover {
color: red;
}
p {
color: pink;
}
}
</style>
</div>
Here, we have a div
with an inline style
tag nested within. This style
tag contains the scoped styles that will apply to this parent div
and its child elements.
Here's the output:
In the example above, the style tag is nested within the .inline-scope
div
, which serves as the scoping root. Therefore, the scope will only apply to the div
and its nested elements.
Browser Support and Limitations
The @scope
at-rule is well-supported across all major browsers except Firefox. It works in Chrome, Safari, and Edge. Scoping is gradually gaining popularity among developers and will be fully supported in all browsers soon.
Check here for more information on browser support.
Conclusion
Scope lets you explicitly decide the regions you want to style in your code. Scoping enhances the developer experience and makes it easier to write code. Developers no longer have to worry about writing long and overly specific class names. There is no risk of name clashes, style conflicts, or CSS styles spilling out and affecting other elements. This article has covered all the basics you need to get started with scoping your CSS.
Resources
If you are interested in reading further and expanding your knowledge on the CSS scope, here are some articles to check out:
Posted on September 29, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.