Skip to main content
Ganesh Joshi
Back to Blogs

Native CSS nesting: write nested selectors without preprocessors

February 21, 20265 min read
Tips
CSS nesting code on screen

CSS nesting lets you write selectors inside other selectors, keeping related styles together and reducing repetition. For years this required Sass, Less, or PostCSS. Now native CSS nesting works in all modern browsers without any build step.

Browser support

Native CSS nesting is fully supported in:

Browser Version
Chrome 112+ (March 2023)
Edge 112+ (March 2023)
Safari 16.5+ (May 2023)
Firefox 117+ (August 2023)

For older browsers, use a PostCSS plugin like postcss-nesting to compile nested CSS to flat selectors during build.

Basic nesting syntax

Wrap child selectors inside a parent:

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

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

  .content {
    color: #666;
    line-height: 1.6;
  }
}

This compiles (conceptually) to:

.card { padding: 1rem; background: white; border-radius: 8px; }
.card .title { font-size: 1.25rem; font-weight: 600; }
.card .content { color: #666; line-height: 1.6; }

The nested selectors inherit the parent context automatically.

The & selector

The & symbol explicitly references the parent selector. It's required for:

Pseudo-classes and pseudo-elements

.button {
  background: blue;
  color: white;

  &:hover {
    background: darkblue;
  }

  &:focus-visible {
    outline: 2px solid orange;
  }

  &::after {
    content: '→';
    margin-left: 0.5rem;
  }
}

Compound selectors

.nav-link {
  color: gray;

  &.active {
    color: blue;
    font-weight: bold;
  }

  &[aria-current="page"] {
    border-bottom: 2px solid blue;
  }
}

Combinators

.list {
  & + .list {
    margin-top: 1rem;
  }

  & > li {
    padding: 0.5rem;
  }

  & ~ .footer {
    border-top: 1px solid #eee;
  }
}

When & is optional

For descendant selectors (space), the & is optional in the latest spec:

/* These are equivalent */
.card {
  .title { font-size: 1.25rem; }
}

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

Use explicit & for clarity or when the meaning might be ambiguous.

Nesting at-rules

You can nest media queries and other at-rules inside selectors:

.sidebar {
  width: 300px;

  @media (max-width: 768px) {
    width: 100%;
    position: fixed;
  }
}

This outputs:

.sidebar { width: 300px; }
@media (max-width: 768px) {
  .sidebar { width: 100%; position: fixed; }
}

You can also nest container queries:

.card {
  padding: 1rem;

  @container (min-width: 400px) {
    padding: 2rem;
    display: grid;
  }
}

Deep nesting

You can nest multiple levels:

.nav {
  display: flex;

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

    .item {
      padding: 0.5rem 1rem;

      &:hover {
        background: #f0f0f0;
      }

      .icon {
        margin-right: 0.5rem;
      }
    }
  }
}

Avoid nesting too deep. Three to four levels is usually the maximum for maintainability. Deep nesting creates high-specificity selectors that are hard to override.

Differences from Sass

Native CSS nesting is similar to Sass but has some differences:

Feature Sass Native CSS
Variables $color: blue; --color: blue; (custom properties)
Mixins @mixin, @include Not supported
Functions darken(), lighten() color-mix(), oklch()
Extend @extend Not supported
Parent reference & anywhere & with some restrictions
Imports @import, @use Native @import or build tools

For features like mixins, you'll need to use other CSS techniques or keep using Sass.

Migrating from Sass

Most Sass nesting patterns work in native CSS:

/* Sass */
.button {
  &:hover { background: darkblue; }
  &.primary { background: blue; }
  .icon { margin-right: 0.5rem; }
}

/* Native CSS - identical */
.button {
  &:hover { background: darkblue; }
  &.primary { background: blue; }
  .icon { margin-right: 0.5rem; }
}

Watch for edge cases:

/* Sass - works */
.parent {
  .child & { color: red; } /* .child .parent */
}

/* Native CSS - not supported */
/* Use flat selectors instead */
.child .parent { color: red; }

Common patterns

Component variants

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

  &.primary {
    background: blue;
    color: white;
  }

  &.secondary {
    background: gray;
    color: white;
  }

  &.ghost {
    background: transparent;
    border: 1px solid currentColor;
  }

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

Responsive components

.hero {
  padding: 2rem;
  text-align: center;

  .title {
    font-size: 2rem;
  }

  .subtitle {
    font-size: 1rem;
    color: #666;
  }

  @media (min-width: 768px) {
    padding: 4rem;

    .title {
      font-size: 3rem;
    }

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

State-based styling

.input {
  border: 1px solid #ccc;
  padding: 0.5rem;

  &:focus {
    border-color: blue;
    outline: none;
  }

  &:invalid {
    border-color: red;
  }

  &:disabled {
    background: #f5f5f5;
    cursor: not-allowed;
  }
}

Combining with other modern CSS

Native nesting works well with other modern CSS features:

.card {
  container-type: inline-size;
  
  .content {
    display: block;

    @container (min-width: 400px) {
      display: grid;
      grid-template-columns: 1fr 2fr;
    }
  }

  &:has(.image) {
    .content {
      padding-left: 1rem;
    }
  }
}

Using with Tailwind v4

Tailwind CSS v4 uses native CSS and Lightning CSS. You can use nesting in your CSS files:

@layer components {
  .card {
    @apply bg-white rounded-lg shadow;

    .title {
      @apply text-xl font-bold;
    }

    &:hover {
      @apply shadow-lg;
    }
  }
}

Performance considerations

Native CSS nesting has no performance cost. Browsers parse nested CSS directly. Unlike Sass, there's no compilation step at build time. The browser's CSS parser handles nesting as efficiently as flat selectors.

Deeply nested selectors do have higher specificity, which can affect cascade performance in very large stylesheets. Keep nesting shallow for maintainability and performance.

Fallbacks for older browsers

If you need to support older browsers, use PostCSS with postcss-nesting:

npm install postcss postcss-nesting

Configure in postcss.config.js:

module.exports = {
  plugins: [
    require('postcss-nesting'),
  ],
};

The plugin compiles nested CSS to flat selectors during build, so older browsers receive compatible CSS.

Summary

Native CSS nesting eliminates the need for preprocessors in many projects. Use & for pseudo-classes, compound selectors, and combinators. Nest media queries and container queries inside selectors for component-scoped responsive design. Keep nesting shallow (3-4 levels max) for maintainability. For older browsers, PostCSS can compile nested CSS to flat selectors.

Frequently Asked Questions

Native CSS nesting lets you write selectors inside other selectors directly in CSS, similar to Sass or Less. Modern browsers support it natively, so you don't need a preprocessor or build step.

Chrome 112+, Safari 16.5+, Firefox 117+, and Edge 112+ support CSS nesting. All modern evergreen browsers have full support as of early 2024.

The & symbol references the parent selector. Use it for pseudo-classes like &:hover, compound selectors like &.active, and combinators. In some cases it's optional for descendant selectors.

CSS nesting has stricter rules about where & can appear. Some Sass patterns like @extend and @mixin don't exist in native CSS. Most common nesting patterns work the same, but edge cases may need adjustment.

For new projects targeting modern browsers, native CSS nesting works well. For existing Sass projects, migration isn't urgent since Sass offers variables, mixins, and functions that native CSS doesn't fully replace.

Related Posts