Frontend Masters Boost RSS Feed https://frontendmasters.com/blog Helping Your Journey to Senior Developer Mon, 05 Jan 2026 17:46:37 +0000 en-US hourly 1 https://wordpress.org/?v=6.9 225069128 How to @scope CSS Now That It’s Baseline https://frontendmasters.com/blog/how-to-scope-css-now-that-its-baseline/ https://frontendmasters.com/blog/how-to-scope-css-now-that-its-baseline/#comments Mon, 05 Jan 2026 17:46:37 +0000 https://frontendmasters.com/blog/?p=8146 Firefox 146 now supports @scope in CSS, joining Chrome and Safari, meaning that it’s now supported in all major web browsers, earning it the “Baseline: Newly Available” tag.

This @scope at-rule defines a new scope context in CSS. The :scope pseudo-class represents the root of said context (otherwise known as the ‘scope root’), and all this means is that we have some new and exciting ways of writing and organizing CSS, so today I’ll demonstrate the different ways of using @scope and the benefits of each one.

You can use an @scope block in any CSS, be it within a stylesheet that you <link> up or a <style> block within HTML. In fact, the latter has some interesting properties we’ll get to.

Whichever way you use CSS, the rules are, by default, globally scoped. For example, in the demo below, even though the CSS rules are nested within the <main>, they apply to the whole document:

<header></header>

<main>
  <style>
    /* No scoping applied. Global scope. */
    header, footer {
      background: rgb(from green r g b / 30%);
    }
  </style>

  <header></header>
  <section></section>
  <footer></footer>
</main>

<footer></footer>

Styles in a CSS stylesheet are also globally scoped by default.

@scope in a <style> Block

However, @scope can be used to limit the CSS to the ‘scope root’ (which in this case is <main>, because the <style> is a direct child of <main>). In addition, within the @scope at-rule, :scope selects this scope root:

<header></header>

<main>
  <style>
    @scope {
      /* Scope root */
      :scope { /* Selects the <main> */

        /* <header>/<footer> within scope root */
        header, footer {
          background: rgb(from green r g b / 30%);
        }
      }
    }
  </style>

  <header></header>
  <section></section>
  <footer></footer>
</main>

<footer></footer>

If needed, we can narrow the scope, or actually, stop the scoping at a particular second selector. In the example below I’ve added a ‘scope limit’ — specifically, I’ve defined the scope as from <main> (implicitly) to <section>. The scope is non-inclusive, so :scope > * selects the <header> and <footer> (excluding <section>, since it’s outside of the scope). main and section don’t select anything as, again, they’re out-of-scope, but we can continue to select <main> by using :scope:

<main>
  <style>
    @scope to (section) {
      /* Selects nothing */
      main, section {
        color: red;
      }

      /* However, this selects <main> */
      :scope {
        font-weight: bold;
      }

      /* Selects scoped direct children */
      :scope > * {
        background: rgb(from green r g b / 30%);
      }

      /* This also works */
      & > * {
        background: rgb(from green r g b / 30%);
      }

      /* As does this */
      > * {
        background: rgb(from green r g b / 30%);
      }
    }
  </style>

  <header></header>
  <section></section>
  <footer></footer>
</main>

When you use the to keyword it is known as donut scope. In the image below you can see why (the rings of the donuts include the scope root and scope limit, but it’s what’s in the donut hole that’s actually included in the scope):

Nothing inside that <section> could be selected at all, because the “donut” stops there. That’s the hole in the donut.

The <style> element itself can be selected by the universal selector (*), so if you were to, for example, set display to anything other than none, the CSS would hilariously output as a raw string (but still work, somehow):

To get really weird, style the <style> element like you would a <pre> tag and add the contenteditable attribute!

Notably, you don’t need to use the :scope selector or an equivalent, that’s just helpful for clarity or adding specificity to the selector if needed.

<p>
  <style>
    @scope {
      color: red;
    }
  </style>
  
  I'll be red.
</p>

<p>I'll be whatever color is inherited.</p>

The potential benefits of scoping in a <style> block are:

  • HTML and CSS are kept together
  • No external resource to download (even if you load CSS asynchronously to stop it from render-blocking, which risks Cumulative Layout Shift anyway, external resources must be downloaded in full before they can be rendered, which isn’t ideal)
  • CSS always renders with the HTML, which means no Cumulative Layout Shift, and when not deferring non-critical CSS, is the best way to prioritize resources efficiently

Keep in mind that the CSS will output more times than is necessary if you’re reusing the component, which is very anti-DRY and why you’ll want to combine this type of scoped CSS with other types of CSS where appropriate. With that in mind, let’s talk about using @scope with internal and external CSS.

@scope in a CSS file

When using @scope with a CSS file, we must specify the scope root (and optionally the end scope) within the @scope at-rule manually, like this:

@scope (main) to (section) {
  > * {
    background: rgb(from green r g b / 30%);
  }
}

This is actually true for <style> blocks as well. If you specify the scope with a selector (with or without the to selector), it will behave the same way. The distinction with a <style> block is when you don’t specify a selector, the scope becomes the parent element.

The benefit of this method is that it’s DRY, so you won’t be repeating yourself. However, there are quite a few drawbacks:

  • CSS is a render blocking resource
  • Potential Cumulative Layout Shift (if loading external CSS asynchronously)
  • HTML and CSS aren’t kept together

Other ways to use :scope

What’s interesting is that, if we use :scope outside of @scope, it selects the global scope root, which in HTML is <html>:

/* Global scope root */
html { }

/* Selects the same thing */
:root { }

/* Selects the same thing! */
:scope { }

I don’t know why we’d select :scope instead of :root or html, but it makes sense that we can do so, and explains why :scope was supported before @scope.

:scope can also be used in the querySelector(), querySelectorAll(), matches(), and closest() JavaScript DOM APIs, where :scope refers to the element on which the method is called. Take the following HTML markup, for example:

<section>
  <div>
    Child div
    <div>Grandchild div</div>
  </div>
</section>

While trying to select the direct child <div> only:

  • section.querySelectorAll("div").forEach(e => e.style.marginLeft = "3rem") undesirably but expectedly selects both <div>s
  • section.querySelectorAll("> div").forEach(e => e.style.marginLeft = "3rem") doesn’t work (even though, as demonstrated earlier, > div would work in CSS)
  • Luckily, section.querySelectorAll(":scope > div").forEach(e => e.style.marginLeft = "3rem") targets the child div only, as desired
  • section.querySelectorAll("& > div").forEach(e => e.style.marginLeft = "3rem") also works, as it would in CSS

Fun fact, we can also use & instead of :scope:

& {
  /* Instead of html, :root, or :scope */
}

@scope (main) to (section) {
  & {
    /* Instead of :scope */
  }
}

A Well-Balanced Approach to Serving and Writing Scoped CSS

I was really looking forward to @scope, and it securing full browser support in the last minute of 2025 made it my feature of the year. Regardless of what types of websites you build, you’ll find all ways of implementing @scope quite useful, although I think you’ll often use all implementations together, in harmony.

It will depend on the type of website that you’re building and how much you want to balance CSS organization with web performance.

Personally, I like splitting CSS into reusable modules, including them as internal CSS using templating logic only when needed (e.g., forms.css on /contact), and then using in-HTML scoped <style>s for one-time or once-per-page components. That way we can avoid render-blocking external CSS without causing Cumulative Layout Shift (CLS) and still have fairly organized CSS. One thing to consider though is that CSS isn’t cached with these methods, so you’ll need to determine whether they’re worth that.

If you’re building heavy front-ends, caching external CSS will be better and fewer bytes overall, but you can totally serve CSS using all methods at once (as appropriate) and use @scope with all of them.

In any case though, the ultimate benefit is, of course, that we’re able to write much simpler selectors by defining new scope roots.

All in all, the future of CSS could look like this:

/* global.css */
body {
  color: #111;
}

section {
  background: #eee;

  h2 {
    color: #000;
  }
}


/* home.css */
@scope (section.home-only) {
  :scope {
    background: #111;

    h2 {
      color: #fff;
    }
  }
}
<!DOCTYPE html>
<html>

<head>
  <!-- Site-wide styles -->
  <link rel="stylesheet" href="global.css">

  <style>
    /* Reusable BUT critical/above-the-fold, so not for global.css */
    @scope (header) {
      height: 100vh;
    }

    /* Include home.css conditionally */
    {% if template.name == "index" %}{% render "home.css" %}{% endif %}
  </style>

</head>

<body>
  <header>
    <h1>Critical/above-the-fold content</h1>
  </header>

  <main>
    <section>
      <h2>Default section (styles from external CSS)</h2>
    </section>

    <section class="home-only">
      <h2>Home-only section (styles from internal CSS)</h2>
    </section>

    <section class="home-only">
      <h2>Home-only section (styles from internal CSS)</h2>
    </section>

    <section class="home-only">
      <h2>Home-only section (styles from internal CSS)</h2>
    </section>

    <section>
      <style>
        @scope {
          :scope {
            background: #f00;

            h2 {
              color: #fff;
            }
          }
      </style>

      <h2>Unique section (styles from in-HTML CSS)</h2>
    </section>

  </main>
</body>

</html>

This is a very simple example. If we imagine that section.home-only is a much more complex selector, @scope enables us to write it once and then refer to it as :scope thereafter.

]]>
https://frontendmasters.com/blog/how-to-scope-css-now-that-its-baseline/feed/ 2 8146
One Thing @scope Can Do is Reduce Concerns About Source Order https://frontendmasters.com/blog/one-thing-scope-can-do-is-reduce-concerns-about-source-order/ Thu, 20 Mar 2025 15:32:23 +0000 https://frontendmasters.com/blog/?p=5434 There is an already-classic @scope demo about theme colors. Let’s recap that and then I’ll show how it relates to any situation with modifier classes. (The @scope rule is a newish feature in CSS that is everywhere-but-Firefox, but is in Interop 2025, so shouldn’t be too long to be decently usable.)

There are lots of different ways to implement color themes, but imagine a way where you do it with class names. I think it’s a valid way to do it, rather than, say, only responding to system preferences, because the classes might give you some on-page control. So you apply a theme at the top level. Then perhaps you have some elements that have other themes. Perhaps your site footer is always dark.

<body class="theme-light">
  <main>
    ...
  </main>
   
  <footer class="site-footer theme-dark">
    &copy;2025 <a href="/">Frontend Masters</a>
  </footer>
</body>

You set up those classes with colors, including other elements that need colors depending on that theme.

.theme-dark {
  background: black;
  color: white;

  a {
    color: #90caf9;
  }
}

.theme-light {
  background: white;
  color: black;

  a {
    color: #1976d2;
  }
}

There is already a problem with the HTML and CSS above.

The <a> in the footer will have the color of the light theme, not the dark theme. This is that classic @scope demo you’re likely to see a lot (sorry). This is because of source order. The selector .theme-light a has the exact same specificity as .theme-dark a but the light theme comes after so it “wins”.

One change to the above CSS will fix this:

@scope (.theme-dark) {
  background: black;
  color: white;

  a {
    color: #90caf9;
  }
}

@scope (.theme-light) {
  background: white;
  color: black;

  a {
    color: #1976d2;
  }
}

This is referred to as proximity. It’s like a new part of the cascade (Bramus has a nice diagram here). Above, because the specificity is the same in both cases, the closer-in-the-DOM proximity “wins”. And closer meaning “fewest generational or sibling-element hops”. Like:

So, appropriately, the link styling nested under @scope (.theme-dark)wins because the specificity is the same but the proximity of the theme-dark class is closer.

What I like about this is that now the source order for those themes doesn’t matter. That’s nice as sometimes that’s hard to control. A good bundler should maintain source order after building, but perhaps these “variation classes” are in different files and the way they get built and loaded isn’t entirely predictable. Perhaps some lazy loading gets involved or the built files are purposefully spit or who-knows-what. I’ve seen too many “it’s fine on dev but broken on prod” bugs for one lifetime.

Color themes was just an excuse to look at variation classes.

Here’s another example:

.card-big {
  padding: 2rem;
}

.card {
  padding: 1rem;
}

Without even looking at HTML, you might consider this a “mistake” because, probably, card-big is a variation class of .card, except the thing that should be an override (the padding) won’t actually be overridden because of source order and equal specificity. I’d guess we all have some muscle memory for just ordering variation classes properly so this isn’t a problem, but it’s not ideal to me that we have to remember that, and that build tools and loading strategies might interfere anyway.

Here’s some real-world-ish CSS I was playing with where I could use @scope to put my variation class first without worry:

@scope (.card-big) {
  :scope {
    grid-column: span 2;
    display: flex;

    img {
      width: 50%;
      height: 100%;
    }
  }
}

.card {
  img {
    width: 100%;
    aspect-ratio: 16 / 9;
    object-fit: cover;
  }
}

Now if I’ve got two cards…

<div class="card">
  ...
</div>

<div class="card card-big">
  ...
</div>

I can rest easy knowing the .card-big styles will apply and appropriately override because of the proximity of the class to the things I want to style (including itself!)

]]>
5434
Reminder that @scope and HTML style blocks are a potent combo https://frontendmasters.com/blog/reminder-that-scope-and-html-style-blocks-are-a-potent-combo/ https://frontendmasters.com/blog/reminder-that-scope-and-html-style-blocks-are-a-potent-combo/#comments Mon, 07 Oct 2024 16:44:04 +0000 https://frontendmasters.com/blog/?p=4121 There are so many different tools for writing scoped CSS with very different takes on how to go about it. Sometimes it’s only a sub-feature of a tool that does other things. But it’s generally thought of as a concept the requires tooling to accomplish.

Have you ever written a React component that imported scoped styles, perhaps as a CSS module?

import styles from "./MyComponent.module.css";

Or used a Styled Component to push some styles onto a component you’re already defining?

const Button = styled.button``

Maybe your Vue components used <style scoped> blocks within them like you can do with Vue Single File Components out of the box?

<template>
  <button>Submit</button>
</template>

<style scoped>
  button {
    border: 3px solid green;
  }
</script>

Despite the seemingly widely-applicable button selector above, the styles will actually be tightly scoped to the button you see in the template after processing.

Or maybe you use Tailwind to apply styling classes directly to elements and like it partially because you don’t have to “name anything”.

There are a lot of solutions like this out there in the land of building websites. I’m pretty convinced myself that scoped CSS is a good idea:

  • There is little to worry about. Styles won’t leak out and have unintended consequences.
  • It’s possible to do efficient things like not load the styles for components that don’t appear on a particular page at the time of loading.
  • When a component retires, so do it’s styles.
  • Styles are often “co-located” with the component, meaning there is a logical obvious connection between markup and styles.

I’m here to tell you: you can do all this stuff with just HTML and CSS.

And I’m not even talking about Web Components or anything particularly controversial or limiting. Vanilla HTML and CSS.

What you do is dump a <style> block in the HTML at the point you want the styles scoped. Just like this:

<main>

   <div>
     <p>Dum de dum de dum.<p>
   </div>

   <div>
     <p>Hi ho here we go.</p>

     <style>
      @scope { /* Scope is the <div> above, as this is a direct child. */
        :scope { /* This selects the <div> */
          border: 1px solid red;
          
          /* I can use CSS nesting in here, ensuring *everything* is safely scoped */
          p {
            color: red;
          }
        }
      }
     </style>
   </div>

</main>

Here’s an example of using it where one of these three <article>s has a scoped styles variation:

I’m using the scoped styles as a “variation” there, but the whole block of styles of that component could be used like that whether it is a variation or not. It’s a way to apply styling only to a particular branch of the ol’ DOM tree. No tooling required. Any way you produce components that end up in the DOM could be done this way, from basic HTML includes to fancy framework components.

Why isn’t this being used much?

Well, it’s the Firefox support mostly I think. Firefox just straight up doesn’t support it at the time of this writing. I’d say this is a strong candidate for Interop 2025. It looked like it was tried for in 2024 but maybe it was too new or something. But maybe Interop isn’t needed as it appears as if it’s being actively worked on, so maybe it won’t be long, dunno.

Once Firebox support is there, I could imagine this as being highly used as a way to accomplish scoped styles for components. It doesn’t require any tooling or have any limitations on what CSS you can use. I would think that would appeal to any existing CSS scoping tool as it would require them to do much less work and work faster.

CSS @scope can do more things, but this particular feature is my favorite and likely to have the biggest impact over time.

]]>
https://frontendmasters.com/blog/reminder-that-scope-and-html-style-blocks-are-a-potent-combo/feed/ 5 4121