THE OPEN STUDIO / THE EDGE

What changed in modern CSS?

CSS can now respond to components, relationships and complex design systems without as much JavaScript. Learn what each feature was built to solve.

Interoperability · Progressive enhancementPlain HTML routeOpen source

The platform keeps solving old frustrations.

A decade ago, developers relied heavily on preprocessors, utility conventions and JavaScript for tasks that browsers now handle directly. Media queries knew the viewport but not the width of a component. Stylesheets accumulated specificity battles. Parent elements could not respond naturally to a child's state. Complex nested layouts struggled to align.

Newer platform features address those particular problems rather than replacing the fundamentals. Learn what failed before the feature existed, and you'll know when it's actually useful.

Think in components, not screen sizes.

Imagine the same article card appearing once in a narrow sidebar and once across a wide page. Both sit in the same viewport. A media query sees the same screen width for both; a container query can see the available width of each card's containing region.

Narrow cardWide card with room for media and text

In CSS, give the component's parent a container context. The child's query then reacts to that context—not necessarily the window.

.card-region { container-type: inline-size; }
.card { display: block; }
@container (min-width: 30rem) {
  .card { display: grid; grid-template-columns: 12rem 1fr; }
}

Five changes worth knowing.

Container queries

Let components adapt to their own context. Container-size queries are broadly interoperable; newer query variants may need additional support checks.

Subgrid

Lets a nested grid share tracks with its parent, making aligned card headings and footers possible without forcing every item into the same DOM level.

Cascade layers and native nesting

@layer provides explicit priority between groups of author rules, helping avoid endless specificity escalation. Nesting lets authors express related selectors within a CSS rule, without requiring Sass or another preprocessor. Neither feature eliminates the need to understand the cascade.

Relational selectors

:has() lets a parent match based on a descendant or relative condition—such as highlighting a form group when an input is invalid. This can replace some class-toggling JavaScript, but you still need to understand selector cost and state semantics.

Motion and positioning primitives

Scroll-driven animation, view transitions and CSS anchor positioning can replace specific custom scripts. Their individual support profiles differ; use progressive enhancement rather than relying on every feature everywhere.

@layer reset, base, components, utilities;

.card {
  padding: 1rem;
  & :is(h2, h3) { margin-block: 0 1rem; }
}
.form-field:has(input:invalid) {
  border-color: firebrick;
}

/* Before relying on an evolving feature: */
@supports (anchor-name: --tip) {
  /* Optional enhancement, with ordinary positioning fallback */
}

Adopt features by maturity and value.

Use the Web Platform Baseline categories to distinguish “widely available,” “newly available,” and not yet Baseline. Baseline isn't a promise about every device still in circulation, so check your actual audience and support requirements.

  • Start with solid HTML and a usable fallback layout.
  • Use container queries when a component is reused in distinct size contexts.
  • Use layers when stylesheet ownership and ordering become hard to reason about.
  • Use :has() for meaningful relational styling, not as a substitute for proper interaction state.
  • Keep experimental animation, anchor positioning and 3D standards optional.

Progressive enhancement protects people.

Native new features do not automatically guarantee accessibility. A tooltip attached with anchor positioning still needs good semantics, keyboard access and usable dismissal behavior. A view transition still needs a reduced-motion strategy. A container query still needs to accommodate large text and reflow.

Performance differs feature by feature. Container queries may affect style and layout; scroll-linked animations can avoid continuous JavaScript scroll handlers but still cost resources when applied to expensive effects. Measure, don't generalize.

Translate an old workaround.

Break it

Build a component that only responds to @media (min-width: 900px), then place it in a tiny sidebar on a wide monitor. Is the card still usable?

Build it

Replace the viewport dependency with a container query and fallback layout. Organize the CSS into two cascade layers. Then use :has() to style a labeled field when its input is invalid, without replacing native validation or labels.

Go a little deeper.