DEV Community

Bilkeesu Babangida
Bilkeesu Babangida

Posted on

Understanding the Concept of Scoping in CSS

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
   }
 }
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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 scope works in practice

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>
Enter fullscreen mode Exit fullscreen mode

In this code, we have a div with the class .jane and a paragraph nested inside it.

@scope (.jane) {
  p {
    color: grey;
  }
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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:

Donut scope

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>
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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:

specificity issue

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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:

scope proximity

Let’s take a look at another example:

<div class="lightblue">
  <div class="lightpink">
    <button>CLICK ME</button>
  </div>
</div>
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

The .lightpink class will be applied here because it is the closest parent of the button.

Here's the output:

scope proximity

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>
Enter fullscreen mode Exit fullscreen mode

Here, we changed the parent div to .lightpink. The .lightblue div is now the child div.

Here's the output:

scope proximity

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>
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

The :scope selector will style the root element and the p will style the paragraphs within the root.

Here's the output:

scope pseudo class

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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:

Inline scope

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:

Top comments (5)

Collapse
 
annavi11arrea1 profile image
Anna Villarreal

css scope is interesting. Haven't used it yet. thanks for sharing! i'll have to try it out.

Collapse
 
bilkeesu96 profile image
Bilkeesu Babangida

Thank you for the feedback. You should try it out soon ☺️

Collapse
 
annavi11arrea1 profile image
Anna Villarreal

I intend to! you sparked a train of thought and i am now writing LOL

Collapse
 
kurealnum profile image
Oscar

Great post!

Collapse
 
bilkeesu96 profile image
Bilkeesu Babangida

Thank you 😊