Go back to home page of Unsolicited Advice from Tiffany B. Brown

Native CSS Nesting: A Primer

Matryoshka, also known as Russian nesting dolls.
Matryoshka, also known as Russian nesting dolls.

Cross-posted to webinista.com. Please link to that version.

UPDATE April 9, 2023: CSS Nesting is now available in stable versions of Google Chrome and Microsoft Edge. Firefox support is still a question mark at this point. If you'd like to use CSS Nesting today, consider using the graceful degradation technique described at the end of this piece.

Pre-processors such as Sass and Less introduced nesting to CSS development. Nesting refers to the ability to group related CSS rules inside of a parent ruleset. It speeds the process of writing CSS because you don't have to re-type selectors. It can also improve the readability and maintainability of CSS by grouping related styles.

Thanks to its popularity and convenience, nesting is now a native feature of CSS. You get one of the benefits of using Sass or Less without the need for an external library and a build process.

CSS Nesting isn't yet ready for prime-time, though. Support is currently limited to Google Chrome and Microsoft Edge (versions 112 and later), and Safari Technology Preview.

CSS Nesting syntax resembles that of Sass/SCSS and Less, but its grammar has some significant differences. I’ll cover some of those differences in this piece. I'm also assuming that you're familiar with Less or Sass/SCSS and CSS.

Nesting syntax basics

As with SCSS and Less, nested rulesets are contained within a parent declaration block. However, CSS Nesting requires that nested selectors begin with a relative selector or the nesting selector (&). Otherwise, the nested rule is ignored.

.parent {
    color: red;

    /*
    Valid because it begins with a combinator, which is a
    form of relative selector.
    */
    > .descendant {
        border: 1px solid black;
    }

    /*
    Valid because it's equivalent of *.img, which is a complex
    selector. Complex selectors are also a form of relative
    selector.
    */
    .nested {
        font-style: italic;
    }

    /* Not valid. Type (element) selectors are not relative selectors. */
    img {
        box-shadow: 0 0 10px 5px rgba(0 0 0 .5);
    }
}

In example 1, the nested img ruleset is invalid CSS. This syntax is perfectly fine for pre-processors. Less and Sass both transform that selector to .parent img. This syntax does not work for native nesting.

Relative selectors

Type or element selectors are not relative selectors. That's why the img ruleset from example 1 fails.

Relative selectors are a category of CSS selector. They include:

  • id selectors such as #primary;
  • class selectors, e.g. .media__object;
  • attribute selectors, such as [alt] or [rel="noopener"];
  • pseudo-class selectors like :hover, :focus, or :disabled;
  • pseudo-elements such as ::before and ::after;
  • and selectors that begin with a combinator, such as > img or + p.

To match type elements in a nested rule you can either:

  • Use a combinator.
  • Prefix it with the nesting selector.
  • Use the :is() pseudo-class.

Here's a rewritten version of the nested img ruleset from example 1.

/* Equivalent to .parent img */
.parent {
    & img {
        box-shadow: 0 0 10px 5px rgba(0 0 0 .5);
    }
}

Changing img to & img or :is(img) makes the ruleset valid.

Invalid nested rules and their parent rules

Although browsers ignore invalid nested rules, an invalid nested rule does not invalidate its parent ruleset. In this case, elements that match .parent will still have red text. Invalid rules can, however, prevent subsequent nested rules and declarations from being applied. Sibling rulesets or declaration added after the img ruleset would bZ ignored.

Using the nesting selector multiple times

The & nesting selector works a bit like a variable or placeholder for the selector of its parent rule. You can use the nesting selector anywhere in a selector list, and you can use it more than once. Example 2 demonstrates how to style nested unordered lists using the nesting selector.

ul {
    list-style: '➤';

    & & {
        list-style: '➢'
    }
}

Note the use of & & with a space — the descendant combinator — between each ampersand. It's the equivalent of ul ul.

Limitations

CSS Nesting is not a drop-in replacement for pre-processors. Sass/SCSS, for example, uses the & as a concatenation operator within nested rulesets (example 4).

.accordion {
    border: 1px solid #ccc;

    &__trigger {
        background: transparent;
        border: 1px solid transparent;
    }
}

When compiled, Sass converts SCSS to valid CSS (example 5).

.accordion {
    border: 1px solid #ccc;
}

.accordion__trigger {
    background: transparent;
    border: 1px solid transparent;
}

CSS Nesting does not support this at all. You need to type the full .accordion__trigger selector, whether or not you nest those rulesets.

You also can't use the nesting selector to represent pseudo-elements. The following CSS does not work.

.subhead::before {
    content: '\25CE';

  /* Ignored because .subhead::before:hover is an invalid selector. */
  &:hover {
    background: yellow;
  }
}

Nesting requires that the resulting selector is a valid one. Note that pre-processors still compile this to CSS. In both cases, however, browsers will ignore the rule set. After all, .subhead::before:hover is not a valid selector.

Nesting multiple levels

The CSS Nesting specification does not specify a maximum nesting level. In the example that follows, rules are nested four levels deep.

.sports__page {
    background: #000;
    color: whitesmoke;

    & p {
        & a {
            color: #fff;
            padding: 3px;

            &:is(:hover, :focus) {
                color: #fc0;
            }

            &:visited {
                color: #ddd;
            }
        }
    }
}

Unlike pre-processors, nested native CSS does not result in larger CSS files. You may, however, choose to limit nesting depth to maximize the reusability and readability of your CSS.

Specificity

Nesting alone does not increase the specificity of a selector compared to its un-nested equivalent. In other words, the rulesets below have the same specificity.

.sports__page {
    & p {}
}

/* Equivalent, un-nested selector */
.sports__page p {}

The more you nest, however, the more specific your selectors become. Highly-specific selectors can make it difficult to reuse existing CSS to create new layouts and variations of components.

Order of appearance

Nested rules are always handled as though they occur after the parent rule, even if that's not how they're ordered in the source. Consider example 9.

.table__alternating {
    border-collapse: collapse;

    .business & {
        border-block: 1px solid #ccc;
    }

    /* A declaration for .table__alternating */
    border-block: 1px solid #000; 
}

It's the equivalent of the CSS shown in example 10.

.table__alternating {
    border-collapse: collapse;
    border-block: 1px solid #000;
}

.business .table__alternating {
    border-block: 1px solid #ccc;
}

For the sake of readability, group your parent rule's declarations together, at the top of the declaration block.

Nesting @-rules

Yes, you can also nest at-rules within a ruleset. This includes conditional at-rules, such as @media, and @supports. It also includes the @container and @layer rules. The syntax is nearly identical to the way it's done in SCSS and Less.

body {
    font-size: 2rem;

    @media (min-width: 600px) {
        font-size: 3rem;
    }
}

The preceeding CSS equivalent to the CSS in example 12.

body {
    font-size: 2rem;
}

@media (min-width: 600px) {
    body {
        font-size: 3rem;
    }
}

When parsed, the at-rule is applied as though it follows its parent rule. Specificity, cascade, and inheritance behave as you'd expect.

Testing for CSS Nesting support

You can use @supports and its selector() function to conditionally apply CSS in browsers that support nesting.

@supports selector(&) {
  /* CSS rules go here */
}

Since the fallback for nested CSS is to use un-nested CSS, though, this does not make much sense. You'd end up sending about twice as much CSS over the network.

An alternative is to use the CSS Object Model and the supports() function to conditionally load a style sheet that contains nesting if the browser supports CSS Nesting, to load a Sass- or Less-compiled style sheet if the browser does not support CSS Nesting. of the style sheet if it does not.

(() => {
    const css = document.querySelectorAll('[rel="stylesheet"]');

    /*
    Update only if the browser doesn't
    support nesting
    */
    if( !CSS.supports('selector( & )') ) {
        css.forEach((link) => {
            const oldCss = l.getAttribute('href');
            l.setAttribute('href', `not-${oldCss}` );
        });
    }
})();

Bear in mind, however, this technique doesn't yet work with Safari. As of this writing, Safari Technology Preview returns false for CSS.supports('selector( & )').

Can I use it?

CSS Nesting is only available in the development / experimental versions of Chrome and Edge, as of version 112. and Safari Technology Preview also supports nesting. It's likely to ship in Safari 16.5. Its specification is still in flux. Behavior and syntax could change between now and when CSS Nesting ships in stable browser versions. Firefox’ development of this feature has not yet begun. Firefox which still has about 6% of desktop marketshare in the United States.

That said, it is a great time to experiment with CSS Nesting. If Sass or Less is part of your workflow, prepare your .less and .scss files for a shift to native nesting. Eliminate instances where you've used & as a concatenator. Move away from variables in favor of using CSS Custom Properties. Begin rewriting and removing mixins, extends, exports, and functions.

Avoid using CSS Nesting in public-facing, production sites for now, though. Your CSS will fail for most of your audience.