Modern CSS Features: Nesting, :has(), Container Queries, Subgrid

Plain CSS now does jobs that used to need Sass or JavaScript. Native nesting groups related rules, :has() styles a parent based on its children, container queries adapt a component to its own width, and subgrid lines nested items up with the outer grid. All four reached Baseline in 2023, so current browsers support them.

The headline features at a glance
FeatureReplacesBaseline since
NestingSass-style nesting in a preprocessor2023
:has()JavaScript that toggles a parent class2023
Container size queriesViewport media queries inside components2023
SubgridHard-coded heights to align card rows2023

Updated

Each feature below has a demo you can edit, and the container query card responds to a slider so you can watch it change layout without resizing your window. The support table lists the year each feature became Baseline, meaning it works in the latest Chrome, Edge, Firefox and Safari.

Native CSS nesting

A card styled with nested rules

The hover, the title and the dark variant all sit inside .card. & refers to .card itself.

<style>
  .card {
    padding: 16px; border: 1px solid #e5e5e5; border-radius: 12px; max-width: 280px;
    & h3 { margin: 0 0 6px; font-size: 18px }
    & p { margin: 0; color: #475569 }
    &:hover { border-color: #4F98FB }
    &.dark { background: #0f172a; color: #fff; & p { color: #cbd5e1 } }
  }
</style>
<div class="card"><h3>Light card</h3><p>Hover to see the border change.</p></div>
<br>
<div class="card dark"><h3>Dark card</h3><p>Same rules, one extra class.</p></div>

The :has() parent selector

Highlight a field when its checkbox is ticked

No script runs in this frame. The label styles itself because it has a checked input inside.

<style>
  label { display: flex; gap: 8px; align-items: center; padding: 10px 12px; margin: 6px 0; border: 1px solid #e5e5e5; border-radius: 10px }
  label:has(input:checked) { border-color: #4F98FB; background: #eff6ff }
  form:has(input:checked) .hint { display: none }
</style>
<form>
  <label><input type="checkbox"> Add gift wrapping</label>
  <label><input type="checkbox"> Send a receipt by email</label>
  <p class="hint">Tick an option to see the parent react.</p>
</form>

Change a card layout only when it contains an image

Cards without an image keep the single-column layout; cards with one get two columns.

.card:has(> img) {
  display: grid;
  grid-template-columns: 120px 1fr;
  gap: 1rem;
}

Container queries

The CSS behind the slider demo

container-type makes .shelf measurable; the @container rule then switches the card to a row once the shelf is at least 420px wide.

.shelf { container-type: inline-size; }

.product { display: grid; gap: 12px; }

@container (min-width: 420px) {
  .product { grid-template-columns: 140px 1fr; align-items: center; }
}

/* Size relative to the container: 1cqi = 1% of its inline size */
.product h3 { font-size: clamp(1rem, 4cqi, 1.5rem); }

Subgrid

Card rows that line up whatever the text length

Each card spans three rows of the outer grid and uses subgrid for its own rows, so every title, body and button share a line.

<style>
  .grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px }
  .card { display: grid; grid-row: span 3; grid-template-rows: subgrid; gap: 6px; padding: 10px; border: 1px solid #e5e5e5; border-radius: 10px }
  .card h3 { margin: 0; font-size: 15px }
  .card p { margin: 0; font-size: 13px; color: #475569 }
  .card a { align-self: end; color: #2563eb }
</style>
<div class="grid">
  <div class="card"><h3>Starter</h3><p>One site.</p><a href="#">Choose</a></div>
  <div class="card"><h3>Team plan with a much longer name</h3><p>Several sites, shared billing and a longer description that wraps.</p><a href="#">Choose</a></div>
  <div class="card"><h3>Agency</h3><p>Client sites.</p><a href="#">Choose</a></div>
</div>

@layer, :is(), :where(), color-mix() and text-wrap

Cascade layers set the winner by order

The ID selector sits in an earlier layer, so the plain class in the later layer still wins.

<style>
  @layer base, overrides;
  @layer base { #cta { background: #94a3b8 } }
  @layer overrides { .btn { background: #4F98FB; color: #fff } }
  .btn { padding: 10px 16px; border: 0; border-radius: 999px }
</style>
<button id="cta" class="btn">Blue wins from the later layer</button>

:is() and :where() shorten selector lists

:is() takes the specificity of its strongest argument, :where() always counts as zero.

/* Before */
article h2, article h3, aside h2, aside h3 { line-height: 1.2; }

/* After */
:is(article, aside) :is(h2, h3) { line-height: 1.2; }

/* Reset with no specificity, easy to override */
:where(ul, ol)[role="list"] { list-style: none; padding: 0; }

color-mix() for tints and hover shades

Mixing with white or black in the oklab space gives even tints from one brand colour.

<style>
  :root { --brand: #4F98FB }
  .s { display: inline-block; width: 60px; height: 50px; border-radius: 8px }
</style>
<span class="s" style="background: color-mix(in oklab, var(--brand) 25%, white)"></span>
<span class="s" style="background: color-mix(in oklab, var(--brand) 60%, white)"></span>
<span class="s" style="background: var(--brand)"></span>
<span class="s" style="background: color-mix(in oklab, var(--brand) 70%, black)"></span>

text-wrap: balance for headings

The second heading spreads its words evenly across lines instead of leaving one word alone. Browsers limit balancing to short blocks of a few lines.

<style>
  h2 { max-width: 320px; font: 700 22px/1.2 system-ui; margin: 0 0 14px }
  .balanced { text-wrap: balance }
</style>
<h2>Website templates for bakeries and coffee shops</h2>
<h2 class="balanced">Website templates for bakeries and coffee shops</h2>

Browser support (Baseline)

Baseline status (latest Chrome, Edge, Firefox and Safari), checked against MDN in October 2026
FeatureBaselineNotes
:is() and :where()2021Widely available
@layer (cascade layers)2022Widely available
Container size queries (@container, container-type)2023Container style queries are separate and not yet Baseline; check caniuse
color-mix()2023
Subgrid2023
CSS nesting2023Bare element selectors at the start of a nested rule need 2023 or later releases
:has()2023
text-wrap: balance2024Older browsers ignore it and wrap normally

How the & selector works in nesting

Inside a nested block, & stands for the outer selector. &:hover attaches the pseudo-class to the parent itself, & + & targets a sibling of the same kind, and .theme-dark & places the parent inside another context.

A nested selector without & is treated as a descendant, so .card { p { } } means .card p. Browsers released in 2023 and later also accept a bare element name such as p at the start of a nested rule.

Cascade layers in practice

@layer reset, base, components, utilities; declares an order up front. A later layer beats an earlier one whatever the specificity, which ends most fights between a component library and your own overrides.

Styles outside any layer beat every layered style. That is handy when bringing an old stylesheet into a layered setup: leave the legacy rules unlayered at first, then move them in one at a time.

Picking what to adopt first

If your users are on current browsers, nesting and :is() are free wins that only shorten your stylesheet. Container queries pay off once you have reusable components that appear at different widths. Subgrid matters most for card grids and forms with labels.

How it works

  1. Drag the width slider on the container query demo and watch the card switch from stacked to side by side.
  2. Edit any example on the left. The sandboxed result on the right redraws as you type.
  3. Check the support table before you ship. Baseline means all four major engines, not older versions.
  4. Copy the snippet you need. None of them require a build step or a library.

Tips

Frequently asked questions

Do I still need Sass for nesting?

Not for nesting alone. Native nesting covers the common pattern of rules inside rules plus the & selector. Sass still offers mixins, loops and functions, though custom properties and color-mix() have taken over much of what people used it for.

What is the difference between container queries and media queries?

A media query asks about the viewport, usually the window width. A container query asks about a chosen ancestor element. That lets one card component show a compact layout in a narrow sidebar and a wide layout in the main column of the same page.

Is :has() slow?

Browser engines optimised it before shipping and ordinary use is fine. Very broad selectors such as body:has(*) on huge pages force more matching work, so anchor :has() to a specific component class where you can.

What does Baseline mean?

Baseline is a label from the web platform community that marks a feature as working in the current versions of Chrome, Edge, Firefox and Safari. Newly available is the date the last of those shipped it. Widely available comes about thirty months later.

When should I use subgrid instead of a nested grid?

Use subgrid when children of a grid item must line up with the outer grid tracks, such as titles, text and buttons across a row of cards. A plain nested grid has its own independent tracks, so content of different lengths drifts out of line.

Related: CSS selectors cheat sheet, CSS media queries, Flexbox and grid cheat sheet, CSS variables, CSS specificity calculator.

Start for free or see all free tools. We.Inc is free to start, with no card needed. AI building is on paid plans from $20 a month.

More in Guides

We.Inc is an AI-powered website builder you can resell under your own brand. Launch a branded client dashboard, bill on Stripe Connect, and deliver AI-generated websites in minutes. White-label plans start at $99 a month for 25 client sites, with a 7-day free trial and no per-site fees.

Product

Who It's For

Features

Resources

Company

View Sitemap