search
HomeWeb Front-endCSS TutorialSolved by CSS: Donuts Scopes

Solved by CSS: Donuts Scopes

Imagine you have a web component that can show lots of different content. It will likely have a slot somewhere where other components can be injected. The parent component also has its own styles unrelated to the styles of the content components it may hold.

This makes a challenging situation: how can we prevent the parent component styles from leaking inwards?

This isn’t a new problem — Nicole Sullivan described it way back in 2011! The main problem is writing CSS so that it doesn’t affect the content, and she accurately coined it as donut scoping.

“We need a way of saying, not only where scope starts, but where it ends. Thus, the scope donut”.

Even if donut scoping is an ancient issue in web years, if you do a quick search on “CSS Donut Scope” in your search engine of choice, you may notice two things:

  1. Most of them talk about the still recent @scope at-rule.
  2. Almost every result is from 2021 onwards.

We get similar results even with a clever “CSS Donut Scope –@scope” query, and going year by year doesn’t seem to bring anything new to the donut scope table. It seems like donut scopes stayed at the back of our minds as just another headache of the ol’ CSS global scope until @scope.

And (spoiler!), while the @scope at-rule brings an easier path for donut scoping, I feel there must have been more attempted solutions over the years. We will venture through each of them, making a final stop at today’s solution, @scope. It’s a nice exercise in CSS history!

Take, for example, the following game screen. We have a .parent element with a tab set and a .content slot, in which an .inventory component is injected. If we change the .parent color, then so does the color inside .content.

How can we stop this from happening? I want to prevent the text inside of .content from inheriting the .parent‘s color.

Just ignore it!

The first solution is no solution at all! This may be the most-used approach since most developers can live their lives without the joys of donut scoping (crazy, right?). Let’s be more tangible here, it isn’t just blatantly ignoring it, but rather accepting CSS’s global scope and writing styles with that in mind. Back to our first example, we assume we can’t stop the parent’s styles from leaking inwards to the content component, so we write our parent’s styles with less specificity, so they can be overridden by the content styles.

body {
  color: blue;
}

.parent {
  color: orange; /* Initial background */
}

.content {
  color: blue; /* Overrides parent's background */
}

While this approach is sufficient for now, managing styles just by their specificity as a project grows larger becomes tedious, at best, and chaotic at worst. Components may behave differently depending on where they are slotted and changing our CSS or HTML can break other styles in unexpected ways.

Two CSS properties walk into a bar. A barstool in a completely different bar falls over.

Thomas Fuchs

You can see how in this small example we have to override the styles twice:

Shallow donuts scopes with :not()

Our goal then it’s to only scope the .parent, leaving out whatever may be inserted into the .content slot. So, not the .content but the rest of .parent… not the .content… :not()! We can use the :not() selector to scope only the direct descendants of .parent that aren’t .content.

body {
  color: blue;
}

.parent > :not(.content) {
  color: orange;
}

This way the .content styles won’t be bothered by the styles defined in their .parent:

You can see an immense difference when we open the DevTools for each example:

As good as an improvement, the last example has a shallow reach. So, if there were another slot nested deeper in, we wouldn’t be able to reach it unless we know beforehand where it is going to be slotted.

This is because we are using the direct descendant selector (>), but I couldn’t find a way to make it work without it. Even using a combination of complex selectors inside :not() doesn’t seem to lead anywhere useful. For example, back in 2021, Dr. Lea Verou mentioned donut scoping with :not() using the following selector cocktail:

.container:not(.content *) {
  /* Donut Scoped styles (?) */
}

However, this snippet appears to match the .container/.parent class instead of its descendants, and it’s noted that it still would be shallow donut scoping:

TIL that all modern browsers now support complex selectors in :not()! ?

Test: https://t.co/rHSJARDvSW

So you can do things like:
– .foo :not(.foo .foo *) to match things inside one .foo wrapper but not two
– .container :not(.content *) to get simple (shallow) “donut scope”

— Dr Lea Verou (@LeaVerou) January 28, 2021

Donut scoping with @scope

So our last step for donut scoping completion is being able to go beyond one DOM layer. Luckily, last year we were gifted the @scope at-rule (you can read more about it in its Almanac entry). In a nutshell, it lets us select a subtree in the DOM where our styles will be scoped, so no more global scope!

@scope (.parent) {
 /* Styles written here will only affect .parent */
}

What’s better, we can leave slots inside the subtree we selected (usually called the scope root). In this case, we would want to style the .parent element without scoping .content:

body {
  color: blue;
}

.parent {
  color: orange; /* Initial background */
}

.content {
  color: blue; /* Overrides parent's background */
}

And what’s better, it detects every .content element inside .parent, no matter how nested it may be. So we don’t need to worry about where we are writing our slots. In the last example, we could instead write the following style to change the text color of the element in .parent without touching .content:

body {
  color: blue;
}

.parent > :not(.content) {
  color: orange;
}

While it may seem inconvenient to list all the elements we are going to change, we can’t use something like the universal selector (*) since it would mess up the scoping of nested slots. In this example, it would leave the nested .content out of scope, but not its container. Since the color property inherits, the nested .content would change colors regardless!

And voilà! Both .content slots are inside our scoped donut holes:

Shallow scoping is still possible with this method, we would just have to rewrite our slot selector so that only direct .content descendants of .parent are left out of the scope. However, we have to use the :scope selector, which refers back to the scoping root, or .parent in this case:

.container:not(.content *) {
  /* Donut Scoped styles (?) */
}

We can use the universal selector in this instance since it’s shallow scoping.

Conclusion

Donut scoping, a wannabe feature coined back in 2011 has finally been brought to life in the year 2024. It’s still baffling how it appeared to sit in the back of our minds until recently, as just another consequence of CSS Global Scope, while it had so many quirks by itself. It would be unfair, however, to say that it went under everyone’s radars since the CSSWG (the people behind writing the spec for new CSS features) clearly had the intention to address it when writing the spec for the @scope at-rule.

Whatever it may be, I am grateful we can have true donut scoping in our CSS. To some degree, we still have to wait for Firefox to support it. ?

This browser support data is from Caniuse, which has more detail. A number indicates that browser supports the feature at that version and up.

Desktop

Mobile / Tablet

The above is the detailed content of Solved by CSS: Donuts Scopes. For more information, please follow other related articles on the PHP Chinese website!

Statement
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn
Edge Goes Chromium: What Does it Mean for Front-End Developers?Edge Goes Chromium: What Does it Mean for Front-End Developers?Apr 22, 2025 am 10:58 AM

In December 2018, Microsoft announced that Edge would adopt Chromium, the open source project that powers Google Chrome. Many within the industry reacted with

A Gutenburg-Powered NewsletterA Gutenburg-Powered NewsletterApr 22, 2025 am 10:57 AM

I like Gutenberg, the new WordPress editor. I'm not oblivious to all the conversation around accessibility, UX, and readiness, but I know how hard it is to

Using  for Menus and Dialogs is an Interesting IdeaUsing for Menus and Dialogs is an Interesting IdeaApr 22, 2025 am 10:56 AM

Using for a menu may be an interesting idea, but perhaps not something to actually ship in production. See "More Details on "

Automated Visual Regression Testing With PlaywrightAutomated Visual Regression Testing With PlaywrightApr 22, 2025 am 10:54 AM

With visual regression testing, we can update a page, take screenshots before and after the fact, and compare the results for unintended changes. In this article, learn how to set up visual regression testing using Playwright.

CSS Houdini Could Change the Way We Write and Manage CSSCSS Houdini Could Change the Way We Write and Manage CSSApr 22, 2025 am 10:45 AM

CSS Houdini may be the most exciting development in CSS. Houdini is comprised of a number of separate APIs, each shipping to browsers separately, and some

Breaking CSS Custom Properties out of :root Might Be a Good IdeaBreaking CSS Custom Properties out of :root Might Be a Good IdeaApr 22, 2025 am 10:43 AM

CSS Custom Properties have been a hot topic for a while now, with tons of great articles about them, from great primers on how they work to creative tutorials

Using a Mixin to Take the Math out of Responsive Font SizesUsing a Mixin to Take the Math out of Responsive Font SizesApr 22, 2025 am 10:35 AM

Responsive Font Size (RFS) is an engine that automatically calculates and updates the font-size property on elements based on the dimensions of the browser

Differential ServingDifferential ServingApr 22, 2025 am 10:25 AM

There is "futuristic" JavaScript that we can write. "Stage 0" refers to ideas for the JavaScript language that are still proposals. Still, someone might turn

See all articles

Hot AI Tools

Undresser.AI Undress

Undresser.AI Undress

AI-powered app for creating realistic nude photos

AI Clothes Remover

AI Clothes Remover

Online AI tool for removing clothes from photos.

Undress AI Tool

Undress AI Tool

Undress images for free

Clothoff.io

Clothoff.io

AI clothes remover

Video Face Swap

Video Face Swap

Swap faces in any video effortlessly with our completely free AI face swap tool!

Hot Tools

MantisBT

MantisBT

Mantis is an easy-to-deploy web-based defect tracking tool designed to aid in product defect tracking. It requires PHP, MySQL and a web server. Check out our demo and hosting services.

mPDF

mPDF

mPDF is a PHP library that can generate PDF files from UTF-8 encoded HTML. The original author, Ian Back, wrote mPDF to output PDF files "on the fly" from his website and handle different languages. It is slower than original scripts like HTML2FPDF and produces larger files when using Unicode fonts, but supports CSS styles etc. and has a lot of enhancements. Supports almost all languages, including RTL (Arabic and Hebrew) and CJK (Chinese, Japanese and Korean). Supports nested block-level elements (such as P, DIV),

Dreamweaver CS6

Dreamweaver CS6

Visual web development tools

DVWA

DVWA

Damn Vulnerable Web App (DVWA) is a PHP/MySQL web application that is very vulnerable. Its main goals are to be an aid for security professionals to test their skills and tools in a legal environment, to help web developers better understand the process of securing web applications, and to help teachers/students teach/learn in a classroom environment Web application security. The goal of DVWA is to practice some of the most common web vulnerabilities through a simple and straightforward interface, with varying degrees of difficulty. Please note that this software

ZendStudio 13.5.1 Mac

ZendStudio 13.5.1 Mac

Powerful PHP integrated development environment