Donut Scoping: A CSS Problem From 2011, Solved
Building a web component that displays arbitrary content often means exposing a slot for other components to be injected into. This creates a classic styling headache: how do you keep the parent component’s CSS from leaking into the content it hosts? This problem of scoping styles around injected content has been around for over a decade.
In 2011, Nicole Sullivan described this exact scenario and gave it a memorable name: donut scoping. The core challenge is that CSS styles need to apply to the component itself but stop before they reach the slotted content—creating a scope with a hole in it.
“We need a way of saying, not only where scope starts, but where it ends. Thus, the scope donut”.
Despite its long history, the problem remained mostly unaddressed until the introduction of the @scope at-rule. While that rule now offers a clean solution, it’s worth reviewing the workarounds developers used in the years before it became an option.
The “Do Nothing” Approach
The simplest solution is to accept CSS’s global scope. Rather than fighting inheritance, you write the parent’s styles with low specificity so that the child component’s own styles can easily override them. This requires explicitly overriding every inherited property from the content component’s side, which can become a maintenance burden as projects grow. Managing specificity manually gets tedious quickly, and components may behave differently depending on where they are slotted.
Consider that some CSS properties inherit. If a parent has a color set, all text inside the slot will inherit it unless specifically overridden. Working around inheritance for every affected property is both repetitive and fragile.
Shallow Donut Scoping with :not()
A more targeted approach uses the :not() pseudo-class. To prevent styles from reaching directly slotted content, you can scope them to direct descendants of the parent while excluding the slot itself:
This ensures the injected component’s styles aren’t crashed by the parent’s default styles. The difference is visible when inspecting the computed styles—the slot content remains unaffected by the parent’s rules.
However, this technique has limitations. It only works for one level of depth. The direct descendant selector (>) prevents the styles from applying to content nested deeper in the DOM. If another component inside the slotted content has its own slots, this shallow approach cannot handle it.
Even complex selectors inside :not() don’t solve the depth problem. As Dr. Lea Verou noted in 2021, you can write selectors like .container :not(.content *) to get simple, shallow donut scoping, but no selector cocktail manages to handle multiple levels of nesting without prior knowledge of the DOM structure.
Full Donut Scoping with @scope
The @scope at-rule fundamentally changes how we approach this problem. It allows you to define a scope root and limit styles to its subtree. To create a donut scope, @scope also takes a second selector that defines where the scope ends—the hole in the donut.
This has several advantages over the :not() approach:
- It detects every element matching the exclusion selector inside the scope root, no matter how deeply nested it is.
- It removes the need to know in advance where slots will be placed within the DOM structure.
- Styles can be written without the low-specificity contortions required by the global scope approach.
One caveat is that you cannot always rely on a universal selector (e.g., *) within the scope rules to apply styles broadly. Using * would exclude all nested slots from the scope, but their container would remain in scope. Since properties like color inherit, the nested content would still change color regardless of being excluded from the scope.
For shallower donut scopes, you can still mimic the older method. By rewriting the scope exclusion selector to target only direct descendants of .parent, using :scope to reference the scope root, you limit the hole to immediate children only. In this specific shallow case, the universal selector is safe because there is nothing nested that would be inappropriately styled.
Historical Context and Future Outlook
Donut scoping sat unsolved for over a decade. What’s notable is that while most developers accepted global scope as an unavoidable fact, the CSS Working Group clearly kept the idea in mind when designing the @scope spec. Its ability to define both the start and the end of a scope is a direct response to the need Sullivan identified back in 2011.
The latest news is that no browser wait is needed any longer. Firefox 146 adds support for @scope at-rule, bringing the feature to Baseline compatibility. This marks the moment when donut scoping, a feature that was a wannabe for 13 years, is finally supported across all major browsers.



