A style rule may contain other style rules, which saves repeating the enclosing selector on every one of them. It is the CSS feature described at MDN Docs, and it works here as it does in a browser.
#panel {
background-color: black;
text {
color: white;
}
&:hover {
background-color: #222;
}
> .header {
padding: 10px;
}
}
How a nested selector resolves
A nested selector is resolved against the one that encloses it, and the result is an ordinary selector matched the ordinary way.
| Written | Means |
|---|---|
#panel { text { … } } | #panel text |
#panel { &:hover { … } } | #panel:hover |
#panel { &.dark { … } } | #panel.dark |
#panel { > .header { … } } | #panel > .header |
.a, .b { text { … } } | .a text, .b text |
#panel { .list { text { … } } } | #panel .list text |
& stands for the enclosing selector. Without it the nested selector is a descendant of the
enclosing one, so #panel { text { … } } never matches #panel itself. & may appear anywhere,
including after the rest of the selector: .item { #panel & { … } } matches an .item inside
#panel.
A rule nests as deep as you like, and the declarations of each rule along the way still apply to what that rule matches.
Conditions nest too
@media, @supports, @container and @starting-style can be written inside a style rule. The declarations directly inside one
belong to the enclosing selector, so this
#panel {
padding: 10px;
@media (min-width: 600px) {
padding: 20px;
}
}
means the same as this.
#panel {
padding: 10px;
}
@media (min-width: 600px) {
#panel {
padding: 20px;
}
}
Rules nest inside a nested condition as well, and resolve against the same enclosing selector.
:is()
:is() matches an element against any of the selectors it is given, and it is what a resolved
nested selector is built from, so it works wherever a selector does.
:is(#panel, #sidebar) > text {
color: white;
}
Where it can be, it is rewritten to one plain selector per argument, which is why the specificity
is then that of the argument that matched rather than :is()’s own rule of taking its most specific
argument. That only differs when the arguments differ in specificity, as in :is(.a, #b).
Two forms cannot be rewritten and are matched as they stand: an :is() that is not at the start of
its compound selector and whose argument spans a combinator, such as .x:is(.a .b) or the
.dark\:x:where(.dark, .dark *) a Tailwind dark variant produces; and an :is() inside another
functional selector, such as :not(:is(.a, .b)). Those take the specificity of their most specific
argument, as the specification says. :not() takes a complex selector the same way, so
:not(.a .b) is everything that is not a .b under an .a.
:where()
:where() matches whatever :is() would and adds no specificity of its own, which is what makes a
rule written with it easy to override.
:where(#panel, #sidebar) > text {
color: white;
}
One class outranks that rule, where an :is() in its place would have made it an id selector.
Everything in the parentheses is discounted, however many compounds it spans and whatever it holds
— an :is() in there counts for nothing either. The two forms :is() is not rewritten in are
matched as they stand here too, still at no specificity.
Escaped class names
A class name containing characters that a selector would otherwise read as syntax is written with escapes, which is how utility frameworks name theirs. Both forms work, including inside a nested selector.
.hover\:underline { color: red; }
.w-1\/2 { width: 50%; }
.\32 xl\:flex { display: flex; }
Limitations
Only @media, @supports, @container, @starting-style and @scope can be nested inside a style rule.
Any other at-rule there, @layer say, is ignored, and the declarations around it are unaffected.
A nested @container works the way a nested @media does; see container queries.
A nested @scope is different: only its start selector resolves against the enclosing rule, and the
rules inside it are relative to the scoping root; see scoped styles.