CSS Nesting: Complete Guide with Examples

Native CSS nesting is here. For years, nesting was the single most compelling reason developers reached for Sass or Less. Now the browser handles it natively, with zero build tools, zero dependencies, and zero compilation step. This guide covers everything you need to know: syntax, the & selector, nested at-rules, specificity implications, migration from preprocessors, and production best practices.

1. What is CSS Nesting?

CSS nesting allows you to write child, descendant, and contextual style rules inside a parent rule block. Instead of repeating the parent selector across multiple flat rules, you nest related styles together. The feature is defined in the CSS Nesting Module Level 1 specification.

Before nesting, you wrote flat CSS like this:

/* Flat CSS — repetitive selectors */
.card { background: #fff; border-radius: 8px; }
.card .title { font-size: 1.25rem; font-weight: 700; }
.card .title span { color: #6b7280; }
.card .body { padding: 1rem; }
.card .body p { line-height: 1.6; }
.card:hover { box-shadow: 0 4px 12px rgba(0,0,0,0.1); }

With native CSS nesting, the same styles become:

/* Nested CSS — grouped by component */
.card {
  background: #fff;
  border-radius: 8px;

  .title {
    font-size: 1.25rem;
    font-weight: 700;

    span { color: #6b7280; }
  }

  .body {
    padding: 1rem;

    p { line-height: 1.6; }
  }

  &:hover {
    box-shadow: 0 4px 12px rgba(0,0,0,0.1);
  }
}

The browser parses the nested version and applies styles identically to the flat version. No build step, no preprocessor, no extra tooling. This is native to the CSS language.

2. Browser Support

CSS nesting has excellent support across modern evergreen browsers as of early 2026:

BrowserVersionRelease DateNotes
Chrome120+Dec 2023Relaxed parsing (bare type selectors allowed)
Edge120+Dec 2023Same engine as Chrome
Firefox117+Aug 2023Full support including relaxed parsing
Safari17.2+Dec 2023Relaxed parsing in 17.2
Opera106+Dec 2023Chromium-based

Global coverage: Over 90% of users worldwide. The main holdouts are older Safari on iOS 16 and earlier, and IE11 (which is end-of-life).

You can detect support at runtime with @supports:

/* Feature detection for CSS nesting */
@supports selector(&) {
  /* Nesting is supported — use nested rules here */
  .card {
    .title { font-weight: 700; }
  }
}

/* Fallback for older browsers */
@supports not selector(&) {
  .card .title { font-weight: 700; }
}

For build-time fallback, the postcss-nesting plugin compiles nested CSS to flat selectors that work everywhere.

3. Basic Nesting Syntax

The fundamental rule: any style rule can contain other style rules inside its block. The nested rule's selector is combined with the parent's selector to form the full selector.

Simple class and ID selectors

.nav {
  display: flex;
  gap: 1rem;

  .link {
    color: #3b82f6;
    text-decoration: none;
  }

  .link.active {
    font-weight: 700;
    border-bottom: 2px solid currentColor;
  }

  #logo {
    font-size: 1.5rem;
  }
}

The browser desugars this to:

.nav { display: flex; gap: 1rem; }
.nav .link { color: #3b82f6; text-decoration: none; }
.nav .link.active { font-weight: 700; border-bottom: 2px solid currentColor; }
.nav #logo { font-size: 1.5rem; }

Type (element) selectors

Since Chrome 120 and Safari 17.2 (the "relaxed parsing" update), you can nest bare type selectors without the & prefix:

.article {
  line-height: 1.7;

  h2 {
    font-size: 1.5rem;
    margin-top: 2rem;
  }

  p {
    margin-bottom: 1rem;
  }

  ul {
    padding-left: 1.5rem;
    list-style: disc;
  }
}

Earlier browser versions (Chrome 112–119, Safari 16.5–17.1) required & h2 instead of bare h2. The relaxed syntax is now the standard.

Multiple nested rules

You can nest as many rules as you need. Declarations (properties) and nested rules can be freely interleaved:

.btn {
  padding: 0.5rem 1rem;
  border: none;
  border-radius: 4px;
  cursor: pointer;

  .icon {
    margin-right: 0.5rem;
    width: 1em;
    height: 1em;
  }

  .label {
    font-weight: 600;
  }
}

4. The & Nesting Selector

The & selector explicitly represents the parent selector. It is required in some contexts and optional in others. Understanding when and how to use it is the key to mastering CSS nesting.

Appending to the parent (pseudo-classes and pseudo-elements)

When you need to append to the parent selector with no space, use & directly:

.btn {
  background: #3b82f6;
  color: #fff;

  &:hover {
    background: #2563eb;
  }

  &:focus-visible {
    outline: 2px solid #60a5fa;
    outline-offset: 2px;
  }

  &:active {
    transform: scale(0.98);
  }

  &::before {
    content: "";
    display: inline-block;
    width: 8px;
    height: 8px;
    border-radius: 50%;
    margin-right: 0.5rem;
  }

  &:disabled {
    opacity: 0.5;
    cursor: not-allowed;
  }
}

This desugars to .btn:hover, .btn:focus-visible, .btn::before, etc. Without the &, a space would be inserted and the browser would look for a descendant element matching :hover, which is not what you want.

Compound selectors (appending a class)

.btn {
  background: #e5e7eb;

  &.primary {
    background: #3b82f6;
    color: #fff;
  }

  &.danger {
    background: #ef4444;
    color: #fff;
  }

  &.outline {
    background: transparent;
    border: 2px solid currentColor;
  }
}

Desugars to .btn.primary, .btn.danger, .btn.outline. The & concatenates with no space.

Prepending — reversing the nesting direction

Place & after another selector to prepend a parent context:

.title {
  font-size: 1rem;

  /* When .title is inside .hero */
  .hero & {
    font-size: 2.5rem;
    font-weight: 800;
  }

  /* When html has dark mode class */
  .dark & {
    color: #e4e4e7;
  }

  /* Right-to-left context */
  [dir="rtl"] & {
    text-align: right;
  }
}

This desugars to .hero .title, .dark .title, and [dir="rtl"] .title. The & is replaced with the parent selector wherever it appears.

Multiple uses of & in one rule

.item {
  /* .item + .item (adjacent sibling) */
  & + & {
    margin-top: 1rem;
  }

  /* .item.item (double specificity boost) */
  && {
    font-weight: 700;
  }
}

5. Nesting Media Queries and @rules

One of the most practical benefits of CSS nesting is placing @media, @supports, @layer, and other conditional at-rules directly inside a style rule. This keeps responsive logic co-located with the component it affects.

Nested @media

.sidebar {
  display: none;

  @media (min-width: 768px) {
    display: block;
    width: 250px;
  }

  @media (min-width: 1024px) {
    width: 300px;
  }

  @media (prefers-reduced-motion: reduce) {
    transition: none;
  }
}

This desugars to:

.sidebar { display: none; }
@media (min-width: 768px) { .sidebar { display: block; width: 250px; } }
@media (min-width: 1024px) { .sidebar { width: 300px; } }
@media (prefers-reduced-motion: reduce) { .sidebar { transition: none; } }

No more hunting through your stylesheet for scattered media query blocks that reference the same selector.

Nested @supports

.grid-layout {
  display: flex;
  flex-wrap: wrap;

  @supports (display: grid) {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
    gap: 1rem;
  }

  @supports (container-type: inline-size) {
    container-type: inline-size;
  }
}

Nested @layer

.component {
  @layer base {
    display: block;
    padding: 1rem;
  }

  @layer theme {
    background: var(--surface);
    color: var(--text);
  }
}

Combining nested at-rules with nested selectors

.card {
  padding: 1rem;
  display: flex;
  flex-direction: column;

  .title {
    font-size: 1rem;

    @media (min-width: 768px) {
      font-size: 1.25rem;
    }
  }

  .image {
    width: 100%;
    aspect-ratio: 16 / 9;

    @media (min-width: 1024px) {
      aspect-ratio: 21 / 9;
    }
  }

  @media (min-width: 768px) {
    flex-direction: row;
    padding: 2rem;
  }
}

At-rules and selector rules can be nested at any level, and they can be nested inside each other. The browser resolves each combination correctly.

6. Nesting Combinators

CSS combinators (>, +, ~, and the descendant space) work naturally inside nested rules.

Child combinator (>)

.nav {
  > li {
    display: inline-block;

    > a {
      padding: 0.5rem 1rem;
      text-decoration: none;
    }
  }
}

Desugars to .nav > li and .nav > li > a.

Adjacent sibling (+)

.heading {
  margin-bottom: 0.5rem;

  + p {
    margin-top: 0;
    font-size: 1.1rem;
  }

  + .subheading {
    color: #6b7280;
  }
}

Desugars to .heading + p and .heading + .subheading.

General sibling (~)

.toggle:checked {
  ~ .panel {
    display: block;
    opacity: 1;
  }

  ~ .label {
    font-weight: 700;
  }
}

Combining combinators with &

.list-item {
  padding: 0.75rem;
  border-bottom: 1px solid #e5e7eb;

  /* .list-item + .list-item */
  & + & {
    border-top: none;
  }

  /* .list > .list-item */
  .list > & {
    padding-left: 1.5rem;
  }
}

7. Specificity and Nesting

This is the most important section for avoiding bugs. Native CSS nesting and Sass nesting produce different specificity due to how the browser desugars nested selectors.

The :is() wrapping rule

When the browser desugars a nested rule, the parent selector is wrapped in :is() if it is a selector list. The :is() pseudo-class takes the specificity of its most specific argument.

/* What you write */
.card, #featured {
  .title {
    color: red;
  }
}

/* What the browser computes */
:is(.card, #featured) .title {
  color: red;
}
/* Specificity: (1,0,1) — the #featured ID specificity
   applies even when matching via .card */

In Sass, the same nesting would produce two separate rules:

/* Sass output */
.card .title { color: red; }    /* Specificity: (0,2,0) */
#featured .title { color: red; } /* Specificity: (1,0,1) */

With native CSS nesting, .card .title has specificity (1,0,1) because :is(.card, #featured) takes the higher specificity of #featured. This can cause unexpected overrides.

Simple parent selectors (no list)

When the parent is a single selector (not a comma-separated list), no :is() wrapping occurs and specificity works exactly as you would expect:

/* Single parent — no :is() wrapping */
.card {
  .title { color: red; }
}
/* Desugars to: .card .title { color: red; }
   Specificity: (0,2,0) — as expected */

Practical specificity tips

8. Complex Nesting Patterns

Pseudo-classes

.input {
  border: 1px solid #d1d5db;

  &:focus {
    border-color: #3b82f6;
    box-shadow: 0 0 0 3px rgba(59,130,246,0.3);
  }

  &:invalid {
    border-color: #ef4444;
  }

  &:placeholder-shown {
    color: #9ca3af;
  }

  &:not(:placeholder-shown):valid {
    border-color: #22c55e;
  }
}

Pseudo-elements

.tooltip {
  position: relative;

  &::before {
    content: attr(data-tip);
    position: absolute;
    bottom: 100%;
    left: 50%;
    transform: translateX(-50%);
    padding: 0.25rem 0.5rem;
    background: #1f2937;
    color: #fff;
    font-size: 0.75rem;
    border-radius: 4px;
    white-space: nowrap;
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.2s;
  }

  &:hover::before {
    opacity: 1;
  }
}

Attribute selectors

.form-field {
  margin-bottom: 1rem;

  [type="text"], [type="email"], [type="password"] {
    width: 100%;
    padding: 0.5rem;
    border: 1px solid #d1d5db;
    border-radius: 4px;
  }

  [required]::after {
    content: " *";
    color: #ef4444;
  }

  &[data-state="error"] {
    .label { color: #ef4444; }
    .message { display: block; }
  }
}

Nesting inside :is(), :has(), and :where()

.card {
  background: #fff;

  &:has(.image) {
    /* Card contains an image */
    grid-template-rows: auto 1fr;

    .title {
      font-size: 1.25rem;
    }
  }

  &:has(> .badge) {
    padding-top: 2rem;
  }
}

Real-world component example

.dropdown {
  position: relative;
  display: inline-block;

  .trigger {
    padding: 0.5rem 1rem;
    border: 1px solid #d1d5db;
    border-radius: 4px;
    cursor: pointer;

    &:hover { background: #f9fafb; }

    &::after {
      content: "\25BC";
      margin-left: 0.5rem;
      font-size: 0.7em;
    }
  }

  .menu {
    position: absolute;
    top: 100%;
    left: 0;
    min-width: 200px;
    background: #fff;
    border: 1px solid #e5e7eb;
    border-radius: 4px;
    box-shadow: 0 4px 12px rgba(0,0,0,0.1);
    display: none;

    .item {
      padding: 0.5rem 1rem;

      &:hover { background: #f3f4f6; }
      & + .item { border-top: 1px solid #f3f4f6; }
    }
  }

  &.open .menu {
    display: block;
  }
}

9. Migrating from Sass/Less to Native CSS Nesting

If you are coming from Sass or Less, most nested code transfers directly. However, there are critical differences to know.

Differences at a glance

FeatureSass/LessNative CSS
Bare type selectorsdiv { } worksWorks in Chrome 120+, Safari 17.2+, Firefox 117+
String concatenation&__element creates .block__elementNot supported. &__element is invalid
SpecificityFlat output, normal specificity:is() wrapping for selector lists
Variables$var / @varvar(--custom-prop)
Mixins@include / .mixin()No equivalent (use custom properties or @apply proposal)
Nesting limitNo limitNo limit (but 3 levels recommended)

The BEM concatenation problem

This is the biggest migration gotcha. Sass allows string concatenation with &:

/* Sass — works fine */
.block {
  &__element { padding: 1rem; }
  &--modifier { color: red; }
}
/* Output: .block__element, .block--modifier */

Native CSS does not support this. The & represents the full parent selector as a selector object, not a string. You must use flat selectors or restructure:

/* Native CSS — correct approach */
.block {
  .block__element { padding: 1rem; }
  .block--modifier { color: red; }
}

/* Or use data attributes instead of BEM */
.block {
  [data-element="title"] { padding: 1rem; }
  &[data-variant="alt"] { color: red; }
}

Migration strategy

  1. Start with new components. Write new CSS using native nesting. Do not rewrite your entire codebase at once.
  2. Remove Sass nesting that uses string concatenation. Replace &__element with explicit class selectors.
  3. Replace Sass variables with CSS custom properties where possible. See our CSS Variables Complete Guide.
  4. Test specificity. Run your existing test suite or visual regression tests, since :is() wrapping can change cascade behavior.
  5. Use PostCSS as a bridge. The postcss-nesting plugin compiles native nesting syntax to flat CSS, giving you the new syntax with old-browser support.

Side-by-side comparison

/* Sass */
.nav {
  display: flex;
  &__list {
    list-style: none;
    &--horizontal { flex-direction: row; }
  }
  &__item {
    &:hover { background: #f0f0f0; }
    &.active { font-weight: bold; }
  }
}

/* Native CSS equivalent */
.nav {
  display: flex;

  .nav__list {
    list-style: none;

    &.nav__list--horizontal { flex-direction: row; }
  }

  .nav__item {
    &:hover { background: #f0f0f0; }
    &.active { font-weight: bold; }
  }
}

10. Performance Considerations

Native CSS nesting has no meaningful performance penalty compared to flat CSS. The browser's style engine has been parsing and matching selectors for decades, and the desugaring step is trivial.

Use our CSS Minifier to compress your nested stylesheets for production, and our CSS Beautifier to format them for development.

11. Best Practices

Limit nesting depth to 3 levels

/* Good: 2 levels */
.card {
  .header {
    .title { font-size: 1.25rem; }
  }
}

/* Bad: 5 levels — hard to read, high specificity */
.page {
  .main {
    .section {
      .card {
        .title { font-size: 1.25rem; }
      }
    }
  }
}

Keep declarations before nested rules

/* Good: declarations first, nested rules after */
.card {
  padding: 1rem;
  background: #fff;
  border-radius: 8px;

  .title { font-weight: 700; }
  .body { line-height: 1.6; }
}

/* Avoid: interleaving declarations and nested rules */
.card {
  padding: 1rem;
  .title { font-weight: 700; }
  background: #fff;
  .body { line-height: 1.6; }
  border-radius: 8px;
}

Use & explicitly for clarity

Even though bare type selectors are allowed, consider using & when the intent might be ambiguous:

.widget {
  /* Clear: this is .widget:hover */
  &:hover { background: #f9fafb; }

  /* Clear: this is .widget.active */
  &.active { border-color: #3b82f6; }

  /* Bare nesting is fine for descendants */
  .title { font-size: 1.1rem; }
}

Group related rules

.btn {
  /* Base styles */
  display: inline-flex;
  align-items: center;
  padding: 0.5rem 1rem;
  border-radius: 4px;

  /* Variants */
  &.primary { background: #3b82f6; color: #fff; }
  &.secondary { background: #6b7280; color: #fff; }
  &.ghost { background: transparent; color: #3b82f6; }

  /* States */
  &:hover { filter: brightness(1.1); }
  &:active { transform: scale(0.98); }
  &:disabled { opacity: 0.5; pointer-events: none; }

  /* Children */
  .icon { margin-right: 0.5rem; }
  .label { font-weight: 600; }

  /* Responsive */
  @media (max-width: 640px) {
    width: 100%;
    justify-content: center;
  }
}

Debug with DevTools

All major browsers show nested rules in the Styles panel. Chrome DevTools displays the original nested source alongside the computed flat selector, making it easy to trace specificity issues. Use the "Computed" tab to see which rules win the cascade.

Combine with CSS custom properties

.card {
  --card-padding: 1rem;
  --card-radius: 8px;

  padding: var(--card-padding);
  border-radius: var(--card-radius);

  .header {
    padding: calc(var(--card-padding) * 0.75);
    border-bottom: 1px solid #e5e7eb;
  }

  @media (min-width: 768px) {
    --card-padding: 1.5rem;
    --card-radius: 12px;
  }
}

Native CSS nesting combined with CSS custom properties gives you most of what Sass provided, with no build step. Pair these with CSS Grid and Flexbox for a complete, modern CSS architecture.

Summary

Native CSS nesting removes the last major reason most teams reached for a CSS preprocessor. Here are the key points:

  1. No build step required. Native nesting works in every modern browser (Chrome 120+, Firefox 117+, Safari 17.2+).
  2. Use & for pseudo-classes, pseudo-elements, and compound selectors. Bare class and type selectors nest without it.
  3. Nest @media and @supports inside rules to keep responsive and progressive-enhancement styles co-located with components.
  4. Watch specificity with selector lists. The browser wraps parent selector lists in :is(), which takes the highest specificity argument.
  5. No BEM string concatenation. Replace &__element patterns with explicit class selectors.
  6. Limit depth to 3 levels. Keep stylesheets readable and specificity manageable.
  7. Combine with CSS custom properties for a powerful, zero-dependency styling system.

Frequently Asked Questions

What is native CSS nesting and how is it different from Sass nesting?

Native CSS nesting is a built-in browser feature that lets you write nested style rules directly in your stylesheets without any preprocessor. Unlike Sass nesting, which is compiled to flat CSS at build time, native CSS nesting is parsed and applied by the browser at runtime. The syntax is very similar, but there are key differences: native CSS nesting requires the & selector when nesting type selectors in older browsers, and specificity is calculated differently because the browser wraps implicit selectors with :is(). Native CSS nesting works without any build tools, compilers, or dependencies.

Can I nest media queries inside a CSS rule?

Yes, one of the most powerful features of CSS nesting is the ability to nest @media, @supports, @layer, and other at-rules directly inside a style rule. This keeps related responsive styles co-located with the component they affect. For example, you can write .card { padding: 1rem; @media (min-width: 768px) { padding: 2rem; } } and the browser applies the nested media query to the .card selector. This eliminates the need to repeat selectors across separate media query blocks.

Do all browsers support CSS nesting?

As of early 2026, CSS nesting is supported in all major evergreen browsers: Chrome 120+, Firefox 117+, Safari 17.2+, and Edge 120+. This covers over 90% of global browser usage. The relaxed parsing rules (allowing bare type selectors without &) shipped in Chrome 120 and Safari 17.2. For older browsers, you can use a PostCSS plugin like postcss-nesting as a build-time fallback that compiles nested CSS to flat selectors.

How does specificity work with CSS nesting?

When you nest selectors in CSS, the browser desugars them using :is() wrapping for selector lists. For example, .card, #featured { .title { } } becomes :is(.card, #featured) .title { }. The :is() pseudo-class takes the specificity of its most specific argument. This means nested selectors may have higher specificity than you expect if the parent is a selector list containing a high-specificity selector like an ID. When the parent is a single selector, no :is() wrapping occurs and specificity is calculated normally.

How deep should I nest CSS rules?

The widely accepted best practice is to limit nesting to 3 levels deep, matching the guideline used in Sass communities for years. Beyond 3 levels, selectors become hard to read, specificity escalates unpredictably, and styles become tightly coupled to HTML structure. Deep nesting also makes debugging harder because browser DevTools show the desugared flat selectors, not the nested source. If you find yourself nesting deeper than 3 levels, refactor using BEM-style flat classes, CSS custom properties for shared values, or break the component into smaller pieces.

Related Resources

CSS Grid Complete Guide
Master two-dimensional layouts with CSS Grid
CSS Variables Complete Guide
Custom properties for dynamic theming and design tokens
CSS Flexbox Complete Guide
One-dimensional layout with Flexbox
CSS Beautifier
Format and indent your CSS for readability
CSS Minifier
Compress CSS for production deployment