Introduction
For over a decade, CSS preprocessors like Sass and Less gave developers the ability to nest selectors, writing more organized and readable stylesheets. The trade-off was always a build step — native CSS could not nest selectors, so you needed a compiler. That changed in 2023 when all major browsers shipped support for native CSS nesting. You can now write nested selectors directly in your stylesheets without any preprocessor, build tool, or runtime cost.
Native CSS nesting uses the & ampersand symbol, just like Sass, but with some important differences in syntax and behavior. The nesting combinator is more flexible — & can appear anywhere in a selector, not just at the beginning — and the feature integrates with the cascade in ways that preprocessors cannot replicate. This guide covers the complete nesting syntax, highlights the differences from Sass, demonstrates practical patterns for real-world projects, and shows how to adopt nesting incrementally alongside your existing stylesheets.
Understanding CSS Nesting: Core Concepts
Basic Nesting Syntax
The fundamental syntax places child selectors inside a parent rule block using &:
.card {
background: white;
border-radius: 8px;
& .title {
font-size: 1.25rem;
font-weight: 700;
}
& .body {
line-height: 1.6;
color: #555;
}
}The & symbol represents the parent selector. In the example above, & .title compiles to .card .title. Without &, the nesting still works but the relationship is implicit:
.card {
background: white;
.title {
/* Equivalent to .card .title */
font-size: 1.25rem;
}
}However, bare element selectors like h2 without & can be ambiguous. The spec requires & when the nested selector could be parsed as a declaration. As a best practice, always include & for clarity.
The & Placement Rules
Unlike Sass where & must come first, native CSS nesting allows & anywhere in the selector:
.link {
color: blue;
/* & at the end — parent pseudo-class */
&:hover {
color: darkblue;
text-decoration: underline;
}
/* & in the middle — compound selector */
.nav & {
font-weight: 600;
}
/* & with pseudo-elements */
&::before {
content: "→";
margin-inline-end: 0.5em;
}
}This flexibility enables patterns that were impossible or awkward in Sass, like qualifying a parent selector against a broader context.
Nesting Without &
Modern browsers also support a shorthand where & is implicit if the nested selector starts with a symbol that cannot be confused with a property name:
.card {
background: white;
/* Implicit & — starts with . which cannot be a property */
.title { font-size: 1.25rem; }
/* Implicit & — starts with [ which cannot be a property */
[data-featured] { border: 2px solid gold; }
/* Must use & — starts with a letter that could be a property */
& div { padding: 1rem; }
}To avoid confusion, I recommend always using the explicit & prefix. It makes the intent clear and prevents subtle bugs.
@nest — The Explicit Nesting Rule
The spec also defines @nest for cases where the nesting relationship is non-obvious:
.parent {
color: black;
@nest .context & {
color: red; /* .context .parent */
}
}However, @nest is largely superseded by the more flexible & placement. It remains in the spec for completeness but is not widely used.
Architecture and Design Patterns
Pattern 1: Component-Scoped Nesting
Group all styles for a component under a single root selector:
.search-form {
display: flex;
gap: 0.5rem;
padding: 1rem;
& input {
flex: 1;
padding: 0.5rem 0.75rem;
border: 1px solid #ccc;
border-radius: 4px;
&:focus {
outline: 2px solid blue;
outline-offset: 2px;
}
&::placeholder {
color: #999;
}
}
& button {
padding: 0.5rem 1.5rem;
background: var(--brand);
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
&:hover { opacity: 0.9; }
&:active { transform: scale(0.98); }
}
}Pattern 2: State-Driven Variants
.alert {
padding: 1rem;
border-radius: 6px;
border: 1px solid transparent;
&.success {
background: #d4edda;
border-color: #c3e6cb;
color: #155724;
}
&.error {
background: #f8d7da;
border-color: #f5c6cb;
color: #721c24;
}
&.warning {
background: #fff3cd;
border-color: #ffeeba;
color: #856404;
}
}Pattern 3: Media Query Nesting
Nest media queries inside selectors for co-located responsive styles:
.grid {
display: grid;
grid-template-columns: 1fr;
gap: 1rem;
@media (min-width: 640px) {
grid-template-columns: repeat(2, 1fr);
}
@media (min-width: 1024px) {
grid-template-columns: repeat(3, 1fr);
gap: 2rem;
}
}This is arguably the biggest quality-of-life improvement over flat CSS. Instead of scattering responsive rules across multiple @media blocks, each component owns its own breakpoints.
Pattern 4: Pseudo-Element Chains
.tooltip {
position: relative;
&::before {
content: attr(data-tooltip);
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
padding: 0.5rem;
background: #333;
color: white;
border-radius: 4px;
font-size: 0.875rem;
white-space: nowrap;
opacity: 0;
pointer-events: none;
transition: opacity 0.2s;
}
&::after {
content: "";
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
border: 5px solid transparent;
border-top-color: #333;
opacity: 0;
transition: opacity 0.2s;
}
&:hover::before,
&:hover::after {
opacity: 1;
}
}Step-by-Step Implementation
Step 1: Verify Browser Support
CSS nesting is supported in Chrome 120+, Firefox 117+, and Safari 17.2+. For older browsers, you need a fallback:
/* Fallback for older browsers */
.card .title { font-size: 1.25rem; }
/* Enhanced with nesting */
@supports selector(&) {
.card {
& .title { font-size: 1.25rem; }
}
}Step 2: Start Nesting New Components
Begin with new code. Every new component file should use nesting:
/* New component — uses nesting from day one */
.avatar-group {
display: flex;
& .avatar {
inline-size: 40px;
block-size: 40px;
border-radius: 50%;
border: 2px solid white;
margin-inline-start: -10px;
&:first-child { margin-inline-start: 0; }
}
}Step 3: Nest State Variants
.toggle {
position: relative;
inline-size: 48px;
block-size: 24px;
background: #ccc;
border-radius: 12px;
cursor: pointer;
transition: background 0.3s;
&[aria-checked="true"] {
background: var(--brand);
&::after {
transform: translateX(24px);
}
}
&::after {
content: "";
position: absolute;
inset-block-start: 2px;
inset-inline-start: 2px;
inline-size: 20px;
block-size: 20px;
background: white;
border-radius: 50%;
transition: transform 0.3s;
}
}Step 4: Nest Responsive Rules
.hero {
padding-block: 3rem;
padding-inline: 1rem;
@media (min-width: 768px) {
padding-block: 5rem;
padding-inline: 3rem;
}
@media (min-width: 1200px) {
padding-block: 8rem;
padding-inline: 5rem;
}
& h1 {
font-size: 2rem;
@media (min-width: 768px) {
font-size: 3rem;
}
}
}Step 5: Use PostCSS Plugin for Older Browser Support
If you need to support browsers that do not ship native nesting, use the PostCSS nesting plugin:
npm install postcss-nesting --save-dev// postcss.config.js
export default {
plugins: {
'postcss-nesting': {},
},
};Step 6: Combine with Cascade Layers
Nesting works seamlessly with @layer:
@layer components {
.modal {
padding: 2rem;
border-radius: 12px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
& .header {
display: flex;
justify-content: space-between;
align-items: center;
& button {
background: none;
border: none;
cursor: pointer;
}
}
& .footer {
display: flex;
justify-content: flex-end;
gap: 0.5rem;
}
}
}Real-World Use Cases
Use Case 1: Design System Button Component
.btn {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding-block: 0.625rem;
padding-inline: 1.25rem;
border: 1px solid transparent;
border-radius: 6px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
&.primary {
background: var(--brand);
color: white;
&:hover { background: var(--brand-dark); }
}
&.outline {
background: transparent;
border-color: var(--brand);
color: var(--brand);
&:hover {
background: var(--brand);
color: white;
}
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
& .icon {
inline-size: 1em;
block-size: 1em;
}
}Use Case 2: Navigation with Nested States
.nav {
display: flex;
align-items: center;
gap: 2rem;
padding-block: 1rem;
padding-inline: 2rem;
& a {
color: var(--text);
text-decoration: none;
position: relative;
&::after {
content: "";
position: absolute;
inset-block-end: -4px;
inset-inline: 0;
block-size: 2px;
background: var(--brand);
transform: scaleX(0);
transition: transform 0.3s;
}
&:hover::after,
&[aria-current="page"]::after {
transform: scaleX(1);
}
}
}Use Case 3: Form Field with Validation States
.field {
display: flex;
flex-direction: column;
gap: 0.25rem;
& label {
font-weight: 600;
font-size: 0.875rem;
}
& input {
padding: 0.5rem;
border: 1px solid #ccc;
border-radius: 4px;
&:focus {
outline: 2px solid var(--brand);
outline-offset: 1px;
}
}
&[data-invalid] {
& input { border-color: red; }
& .error-msg { display: block; }
}
& .error-msg {
display: none;
color: red;
font-size: 0.75rem;
}
}Best Practices for Production
-
Always use explicit
&: While implicit nesting works in modern browsers, explicit&prevents ambiguity and makes your code self-documenting. Every nested selector should start with&. -
Limit nesting depth: Deep nesting produces overly specific selectors. Aim for a maximum of 3 levels. If you need more, your component structure may need rethinking.
-
Nest media queries inside components: This is the single biggest readability win. Each component owns its responsive behavior, making it easy to find and modify breakpoints.
-
Use
@supportsfor progressive enhancement: Wrap nesting in@supports selector(&)if you need to support older browsers alongside native nesting. -
Prefer nesting over flat BEM: Nesting gives you the scoping benefits of BEM without the naming ceremony. Use it to replace
.card__title--activewith& .title.active. -
Combine with
@layerand@scope: Nesting, layers, and scope are complementary. Layers handle priority, scope handles boundaries, and nesting handles organization. -
Document your nesting conventions: Agree on maximum depth, when to use nesting vs. flat selectors, and whether to nest media queries per-component or per-page.
-
Watch for specificity inflation: Each level of nesting adds specificity. A selector like
.card .title.activehas higher specificity than.card-title-active. Keep nesting shallow to maintain override flexibility.
Common Pitfalls and Solutions
| Pitfall | Impact | Solution |
|---|---|---|
| Nesting too deeply (4+ levels) | Overly specific selectors, hard to override | Limit to 3 levels maximum |
Forgetting & before element selectors | Selector may be parsed as a property | Always prefix with explicit & |
Mixing nesting with @import | @import must appear before any rules including nested ones | Place @import at the top of the file |
Nesting @keyframes inside selectors | @keyframes are not nestable | Define @keyframes at the top level |
| Assuming Sass nesting behavior | Subtle differences in specificity and & placement | Read the CSS nesting spec, not Sass docs |
Nesting @font-face | Not supported | Keep @font-face at the top level |
Sass vs. Native Nesting Differences
The key differences from Sass:
&placement: Sass requires&at the start; native CSS allows it anywhere.- Compilation: Sass compiles at build time; native nesting is resolved by the browser.
- Specificity: Both inflate specificity, but native nesting preserves the original specificity calculation more faithfully.
@import: Sass processes@importduring compilation; native@importhas ordering restrictions.
Performance Optimization
Native CSS nesting has no runtime performance penalty beyond the normal selector matching cost. The browser parses the nested syntax into the same internal representation as flat selectors. However, deeply nested selectors do produce longer selector chains, which are slightly more expensive to match:
/* Shallow — fast to match */
.card-title { font-size: 1.25rem; }
/* Deeply nested — slower to match */
.page .layout .main .card .title { font-size: 1.25rem; }Keep nesting shallow (2-3 levels) for optimal performance. The organizational benefits of nesting should not come at the cost of selector complexity.
Comparison with Alternatives
| Feature | Native CSS Nesting | Sass/Less Nesting | CSS Modules | Tailwind |
|---|---|---|---|---|
| Build step required | No | Yes | Yes | Yes |
& placement flexibility | Anywhere | Start only | N/A | N/A |
| Specificity control | Manual (depth) | Manual (depth) | Hash-based | Utility-based |
| Browser support | 95%+ (2024) | Universal | Universal | Universal |
| File size impact | None (native) | Compiled away | Compiled away | Purged |
| Media query nesting | Yes | Yes | Yes | Via @apply |
Advanced Patterns
Nesting with :is() and :where()
.card {
&:is(.featured, .promoted) {
border: 2px solid gold;
}
&:where(.low-priority) {
opacity: 0.7;
}
}Nesting with :has()
.form-group {
padding: 1rem;
&:has(input:focus) {
background: var(--focus-bg);
}
&:has(input:invalid) {
border-color: red;
}
}Container-Aware Nesting
.card {
container-type: inline-size;
& .layout {
display: flex;
flex-direction: column;
}
@container (min-inline-size: 500px) {
& .layout {
flex-direction: row;
gap: 2rem;
}
}
}Testing Strategies
Test that nested selectors resolve correctly:
import { test, expect } from '@playwright/test';
test('nested hover styles apply correctly', async ({ page }) => {
await page.goto('/demo');
const link = page.locator('.nav a').first();
await link.hover();
const color = await link.evaluate(el => getComputedStyle(el).color);
expect(color).toBe('rgb(0, 0, 255)');
});
test('nested media query produces correct layout', async ({ page }) => {
await page.setViewportSize({ width: 1200, height: 800 });
await page.goto('/demo');
const columns = await page.locator('.grid').evaluate(
el => getComputedStyle(el).gridTemplateColumns
);
expect(columns.split(' ').length).toBe(3);
});Migration from Sass and PostCSS
Migrating from Sass to native CSS nesting requires replacing Sass-specific syntax with standard CSS nesting syntax. The primary differences are the nesting indicator (& is required in native CSS but optional in Sass for simple nesting), the lack of Sass functions and mixins (which require separate CSS solutions), and the absence of Sass variables (replaced by CSS custom properties). Create a migration plan that converts Sass files one at a time, starting with files that use the least Sass-specific functionality.
PostCSS with postcss-nesting or postcss-nested provides a bridge for projects that need to support older browsers while writing nested CSS. Configure PostCSS to transform nested syntax into flat CSS during the build process. Once your browser support requirements allow native nesting, remove the PostCSS plugin and rely on the browser's built-in parser. This approach lets your team write modern CSS syntax immediately while maintaining backward compatibility.
For large Sass codebases, consider a hybrid approach where new files use native CSS nesting and existing files are migrated opportunistically when they need significant changes. This avoids the risk of a big-bang migration while gradually moving toward standard CSS. Use linting rules to prevent new Sass-specific features from being added to existing files, ensuring the codebase moves consistently toward native CSS.
Future Outlook
Native CSS nesting is now baseline across all major browsers. The CSS Working Group is exploring deeper integration with @scope (to scope nested styles to a DOM subtree) and @layer (to combine nesting with cascade layer control). Polyfill tools like postcss-nesting continue to improve, and new CSS tooling (Lightning CSS, Oxlint CSS) supports nesting out of the box. Expect Sass usage to decline as native nesting eliminates the primary reason most teams adopted preprocessors. The next frontier is CSS modules with native nesting, which would combine scoping with the organizational benefits of nesting.
CSS Nesting Performance
Native CSS nesting has no performance overhead compared to writing equivalent flat selectors. The browser parses nested selectors into the same specificity and selector chain as their flat equivalents. However, deeply nested selectors can create high-specificity selectors that are harder to override with utility classes. Keep nesting to 2-3 levels maximum to maintain reasonable specificity. Use the & nesting selector explicitly when referencing the parent to avoid ambiguity, and prefer nesting for component-scoped styles rather than global overrides.
CSS Nesting Migration Tools
Migrate preprocessor-based nesting to native CSS nesting using automated tools. PostCSS plugins like postcss-nesting and postcss-nested can transform Sass or Less nesting syntax into native CSS nesting. Use stylelint rules to enforce consistent nesting patterns and prevent overly deep nesting. When migrating from Sass, replace @extend with native CSS cascade layers and replace Sass mixins with CSS custom properties where possible. Test migrated stylesheets across target browsers to verify compatibility.
Conclusion
Native CSS nesting eliminates the last major reason to use a CSS preprocessor. You get cleaner, more organized stylesheets with co-located responsive rules, state variants, and pseudo-element styles — all without a build step.
Key takeaways:
- Use explicit
&for every nested selector to avoid parsing ambiguity. - Limit nesting to 3 levels to prevent specificity inflation.
- Nest media queries inside components — this is the biggest readability win.
- Use
@supports selector(&)for progressive enhancement when supporting older browsers. - Combine with
@layerand@scopefor a complete modern CSS architecture. - Replace Sass nesting gradually — start with new components, migrate existing ones as you touch them.
Start by nesting your next component's pseudo-classes and media queries. The organizational improvement is immediately apparent, and you can expand from there.