Native CSS Nesting: A Primer

UPDATE NOVEMBER 13, 2023: CSS Nesting is now available in the current version of all major browsers. I've updated the post to reflect this.

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 build process.

CSS Nestingisn'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. is available in the latest versions of all major browsers.

I'm assuming that you're already familiar with Less or Sass/SCSS and CSS.

Nesting syntax basics

CSS Nesting syntax resembles that of Sass/SCSS and Less, but its grammar has some significant differences.

As with SCSS and Less, nested rulesets are contained within a parent declaration block, as demonstrated in example 1.

.parent {
    color: red;

    > .descendant {
        border: 1px solid black;
    }

    .nested {
        font-style: italic;
    }

    img {
        box-shadow: 0 0 10px 5px rgba(0, 0, 0, .5);
    }
}

Earlier versions of the CSS Nesting specification required use of the & nesting selector in order when nesting type selectors such as p and img (see example 2).

.parent {
    color: red;

    /* Previously-required nesting selector */
    & img {
        box-shadow: 0 0 10px 5px rgba(0, 0, 0, .5);
    }
}

More recent versions of the specification have relaxed that rule. Using the nesting selector is now optional in many cases. It's useful to understand how it works.

One note of caution: as of November 13, 2023, only Firefox, Chrome Canary and Safari Technology Preview support the more recent spec. These changes will ship in near-future releases of Safari, Chrome, and Edge.However, CSS Nesting requires that nested selectors begin with a relative selector or the nesting selector (&). Otherwise, the nested rule is ignored.

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.

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. An invalid rule can, however, prevent a subsequent sibling rule from being applied. Sibling rulesets or declaration added after the img ruleset would be ignored.

&: The nesting selector

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.

One such case is when you're targeting a descendant element of the same type as the parent — think nested div or ul elements, as shown in example 3.

ul {
    list-style: '➤';

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

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

Browsers assume that a nested rule matches descendants of the parent selector unless you indicate otherwise. Consider the following example.

p {
    margin-block: 1rem;

    a {
        text-decoration: 2px underline wavy green;
    }
}

The above rules only match a elements that are descendants of p elements. It's equivalent to the following CSS.

p {
    margin-block: 1rem;
}

p a {
  text-decoration: 2px underline wavy green;
}

Of course, descendant selectors aren't the only kind of selectors that exist in CSS. You can also use the & selector to match elements that have a different relationship.

For example, use & to incorporate the parent rule's selector as part of a descendant selector, as shown in example 6.

ul {
    /* The equivalent of `aside ul` */
    aside & {
        list-type: square;
    }
}

Or use & to match child pseudo-elements and qualify pseudo-classes such as :hover and :not().

.fancy-aside {
    display: flex;
    gap: 0.5rem;

    /**
     * equivalent to .fancy-aside::before, .fancy-aside::after
     */
    &::before,
    &::after {
        content: '❊ ❊ ❊';
    }
}

In this case, it's necessary to use &. Without it, our rule would match every pseudo-element descendant of .fancy-aside, including .fancy-aside a::before and .fancy-aside li::after.

& is not a concatenation operator

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

.accordion {
    border: 1px solid #ccc;

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

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

.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 12.

.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 13.

.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 15.

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.

One 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( & )').

Bear in mind, however, that some recently outdated browser versions may return a false negative.

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.

Yes. CSS Nesting is available in every major browser. To be on the safe side, continue to use the nesting selector (&) with type selectors.

That said, it is It's also 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.

CSS has come a long way since its early days. Features like CSS nesting help us write it much more efficiently.

Subscribe to the Webinista (Not) Weekly

A mix of tech, business, culture, and a smidge of humble bragging. I send it sporadically, but no more than twice per month.

View old newsletters