CSS nesting is a convenience syntax addition that allows CSS to be added inside
of a ruleset. If you’ve used
SCSS,
Less or
Stylus, then
you’ve most certainly seen a few flavors of this:
.nesting {
color: hotpink;
> .is {
color: rebeccapurple;
> .awesome {
color: deeppink;
}
}
}
Which after being compiled to regular CSS by the preprocessor, turns into
regular CSS like this:
.nesting {
color: hotpink;
}
.nesting > .is {
color: rebeccapurple;
}
.nesting > .is > .awesome {
color: deeppink;
}
An official CSS version of this syntax is being strongly considered and we have
a split in preference that we’d like to employ the help of the community to
break the tie. The rest of this post will be introducing the syntax options so
you can feel informed to take a survey at the end.
Why can’t the exact nesting example shown above be the syntax for CSS nesting?
Why can’t the exact nesting example shown above be the syntax for CSS nesting?
There are a few reasons the most popular nesting syntax can’t be used as is:
-
Ambiguous parsing
Some nested selectors can look exactly like properties and
preprocessors
are able to resolve and manage them at build time. Browser engines won’t have
the same affordances, selectors needs to never be loosely interpreted. -
Preprocessor parsing conflicts
The CSS way of nesting shouldn’t break
preprocessors or existing developer
nesting workflows. This would be disruptive and inconsiderate to those
ecocystems and communities.
-
Waiting for
:is()
Basic nesting doesn’t need:is()
but more complex nesting does. See Example
#3 for a light introduction to selector
lists and nesting. Imagine that selector list was in the middle of a selector
instead of at the beginning, in those cases:is()
is required in order to
group the selectors in the middle of another selector.
Overview of what we’re comparing
We want to get CSS nesting right, and in that spirit we’re including the
community. The following sections will help describe the three possible versions
we’re evaluating. We’ll then go over some examples of usage for comparison, and
at the end there will be a light survey asking you which you preferred overall.
Option 1: @nest
This is the current specified syntax in CSS Nesting
1. It offers a convenient way to nest
appending styles by starting new nested selectors with &
. It also offers
@nest
as a way to place the &
context anywhere inside a new selector, like
when you’re not just appending subjects. It’s flexible and minimal but at the
expense of needing to remember @nest
or &
depending on your use case.
Option 2: @nest restricted
This is a stricter alternative, in an attempt to reduce the expense mentioned of
remembering two nesting methods. This restricted syntax only allows nesting to
occur following @nest
, so there’s no append only convenience pattern. Removing
ambiguity of choice, creating one easy to remember way to nest, but sacrifices
terseness in favor of convention.
It’s worth noting that the less restricted @nest
syntax (Option 1)
could use a linter to enforce a usage rule across a team, in case they together
decided they didn’t like the ambiguity or terseness. Changing the spec may be an
extreme measure.
Option 3: Brackets
In order to avoid the double-syntax or extra clutter involved with the @nest
proposals, Miriam Suzanne and Elika
Etemad proposed an alternative
syntax
that instead relies on additional curly-brackets. This provides syntax clarity,
with only two extra characters, and no new at-rules. It also allows nested rules
to be grouped by the type of nesting required, as a way of simplifying
multiple similarly nested selectors.
Example 1 – Direct nesting
@nest
.foo {
color: #111;
& .bar {
color: #eee;
}
}
@nest always
.foo {
color: #111;
@nest & .bar {
color: #eee;
}
}
brackets
.foo {
color: #111;
{
& .bar {
color: #eee;
}
}
}
Equivalent CSS
.foo {
color: #111;
}
.foo .bar {
color: #eee;
}
Example 2 – Compound nesting
@nest
.foo {
color: blue;
&.bar {
color: red;
}
}
@nest always
.foo {
color: blue;
@nest &.bar {
color: red;
}
}
brackets
.foo {
color: blue;
{
&.bar {
color: red;
}
}
}
Equivalent CSS
.foo {
color: blue;
}
.foo.bar {
color: red;
}
Example 3 – Selector lists and nesting
@nest
.foo, .bar {
color: blue;
& + .baz,
&.qux {
color: red;
}
}
@nest always
.foo, .bar {
color: blue;
@nest & + .baz,
@nest &.qux {
color: red;
}
}
brackets
.foo, .bar {
color: blue;
{
& + .baz,
&.qux {
color: red;
}
}
}
Equivalent CSS
.foo, .bar {
color: blue;
}
:is(.foo, .bar) + .baz,
:is(.foo, .bar).qux {
color: red;
}
Example 4 – Multiple levels
@nest
figure {
margin: 0;
& > figcaption {
background: lightgray;
& > p {
font-size: .9rem;
}
}
}
@nest always
figure {
margin: 0;
@nest & > figcaption {
background: lightgray;
@nest & > p {
font-size: .9rem;
}
}
}
brackets
figure {
margin: 0;
{
& > figcaption {
background: lightgray;
{
& > p {
font-size: .9rem;
}
}
}
}
}
Equivalent CSS
figure {
margin: 0;
}
figure > figcaption {
background: hsl(0 0% 0% / 50%);
}
figure > figcaption > p {
font-size: .9rem;
}
Example 5 – Parent nesting or subject changing
@nest
.foo {
color: red;
@nest .parent & {
color: blue;
}
}
@nest always
.foo {
color: red;
@nest .parent & {
color: blue;
}
}
brackets
.foo {
color: red;
{
.parent & {
color: blue;
}
}
}
Equivalent CSS
.foo {
color: red;
}
.parent .foo {
color: blue;
}
Example 6 – Mixing direct and parent nesting
@nest
.foo {
color: blue;
@nest .bar & {
color: red;
&.baz {
color: green;
}
}
}
@nest always
.foo {
color: blue;
@nest .bar & {
color: red;
@nest &.baz {
color: green;
}
}
}
brackets
.foo {
color: blue;
{
.bar & {
color: red;
{
&.baz {
color: green;
}
}
}
}
}
Equivalent CSS
.foo {
color: blue;
}
.bar .foo {
color: red;
}
.bar .foo.baz {
color: green;
}
Example 7 – Media query nesting
@nest
.foo {
display: grid;
@media (width => 30em) {
grid-auto-flow: column;
}
}
or explicitly / extended
.foo {
display: grid;
@media (width => 30em) {
& {
grid-auto-flow: column;
}
}
}
@nest always (is always explicit)
.foo {
display: grid;
@media (width => 30em) {
@nest & {
grid-auto-flow: column;
}
}
}
brackets
.foo {
display: grid;
@media (width => 30em) {
grid-auto-flow: column;
}
}
or explicitly / extended
.foo {
display: grid;
@media (width => 30em) {
& {
grid-auto-flow: column;
}
}
}
Equivalent CSS
.foo {
display: grid;
}
@media (width => 30em) {
.foo {
grid-auto-flow: column;
}
}
There’s a convenience syntax called implicit nesting. The top/first
examples in each example show the implicit nesting feature, where the scope of
the style rules are applied to the implicit selector scope they are nested
within. Implicit nesting does need adjusting when more than one nested selector
needs to exist, at which point you’ll need to refactor to the explicit nesting
style.
Example 8 – Nesting groups
@nest
fieldset {
border-radius: 10px;
&:focus-within {
border-color: hotpink;
}
& > legend {
font-size: .9em;
}
& > div {
& + div {
margin-block-start: 2ch;
}
& > label {
line-height: 1.5;
}
}
}
@nest always
fieldset {
border-radius: 10px;
@nest &:focus-within {
border-color: hotpink;
}
@nest & > legend {
font-size: .9em;
}
@nest & > div {
@nest & + div {
margin-block-start: 2ch;
}
@nest & > label {
line-height: 1.5;
}
}
}
brackets
fieldset {
border-radius: 10px;
{
&:focus-within {
border-color: hotpink;
}
}
> {
legend {
font-size: .9em;
}
div {{
+ div {
margin-block-start: 2ch;
}
> label {
line-height: 1.5;
}
}}
}
}
Equivalent CSS
fieldset {
border-radius: 10px;
}
fieldset:focus-within {
border-color: hotpink;
}
fieldset > legend {
font-size: .9em;
}
fieldset > div + div {
margin-block-start: 2ch;
}
fieldset > div > label {
line-height: 1.5;
}
Example 9 – Complex nesting group «Kitchen Sink»
@nest
dialog {
border: none;
&::backdrop {
backdrop-filter: blur(25px);
}
& > form {
display: grid;
& > :is(header, footer) {
align-items: flex-start;
}
}
@nest html:has(&) {
overflow: hidden;
}
}
@nest always
dialog {
border: none;
@nest &::backdrop {
backdrop-filter: blur(25px);
}
@nest & > form {
display: grid;
@nest & > :is(header, footer) {
align-items: flex-start;
}
}
@nest html:has(&) {
overflow: hidden;
}
}
brackets
dialog {
border: none;
{
&::backdrop {
backdrop-filter: blur(25px);
}
& > form {
display: grid;
{
& > :is(header, footer) {
align-items: flex-start;
}
}
}
}
{
html:has(&) {
overflow: hidden;
}
}
}
Equivalent CSS
dialog {
border: none;
}
dialog::backdrop {
backdrop-filter: blur(25px);
}
dialog > form {
display: grid;
}
dialog > form > :is(header, footer) {
align-items: flex-start;
}
html:has(dialog) {
overflow: hidden;
}
Time to vote
Hopefully you feel that was a fair comparison and sample platter of the syntax
options we’re evaluating. Please review them carefully and let us know which you
prefer below. We appreciate you helping us advance CSS nesting to a syntax we
all will come to know and love!
This post is also available in: English