PrimeStack.
Design·Aug 28, 2025

The State of CSS in 2025

From container queries to nesting and scoping – a look at the modern CSS features you can use today.


The state of CSS in 2025 is the best it has ever been. Between 2022 and 2025, the platform shipped more meaningful new features than in the entire previous decade. Container queries, native nesting, the :has() selector, cascade layers, scroll-driven animations, and anchor positioning have collectively eliminated entire categories of JavaScript and preprocessor dependency. If you haven't revisited what vanilla CSS can do since 2021, you're working with an outdated mental model.

This article walks through the features that matter most, how they work, where the gotchas are, and what the browser support landscape actually looks like today.

Table of Contents


The CSS Renaissance: What Changed and Why

The pace of CSS development was historically slow because browser vendors disagreed on priorities and the specification process was fragmented. That changed around 2021 when the major browser engines — Chromium, Firefox, and WebKit — began coordinating through Interop, an annual joint effort to identify and implement the same features simultaneously.

Interop 2022, 2023, and 2024 targeted the exact features that developers had wanted for years: container queries, cascade layers, the :has() selector, CSS nesting, and more. The result is that virtually every feature discussed in this article has reached Baseline status — meaning it works in all major browsers and has for at least a year.

The practical implication: you no longer need to hold back from these features for cross-browser reasons. The question is whether your user base includes older browsers that predate these implementations.


Container Queries: Solving Component-Level Responsive Design

Media queries answer the question "how big is the viewport?" Container queries answer "how big is my parent container?" This distinction matters enormously for component design.

Consider a card component. With media queries, you'd write breakpoints based on viewport width — but a card rendered in a sidebar has completely different space constraints than the same card in a full-width grid. You'd end up with media query overrides specific to the context, coupling the component to the layout. Container queries decouple them.

.card-wrapper {
  container-type: inline-size;
  container-name: card;
}

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

The container-type: inline-size declaration establishes a containment context. Any descendant can then query the container's inline size (width in horizontal writing modes) with @container. The container-name is optional but recommended when components are nested.

Style Queries

Container queries also support querying custom property values — called style queries. This is still experimental in some contexts but is shipping:

@container style(--variant: featured) {
  .card {
    border: 2px solid var(--color-accent);
  }
}

Style queries let parent components communicate state to children through CSS custom properties without needing class names or JavaScript. The practical use case: theme variants, component configuration, and conditional styling based on context rather than structure.


CSS Nesting: Native, Not Sass

CSS nesting lets you nest rules inside other rules, eliminating repetition and keeping related styles co-located. It looks similar to Sass nesting but has important differences.

.button {
  padding: 0.5rem 1rem;
  background: var(--color-primary);

  &:hover {
    background: var(--color-primary-dark);
  }

  &.button--large {
    padding: 0.75rem 1.5rem;
  }

  .button__icon {
    margin-inline-end: 0.5rem;
  }
}

The & represents the current selector. The .button__icon rule without & works in modern implementations but had inconsistent behavior in early browser implementations — if you're targeting broader compatibility, always include & for element and class selectors nested inside.

Gotchas

The & selector has higher specificity than you might expect when combined with pseudo-classes or class selectors. Also, nesting doesn't flatten in the same way Sass does — the generated CSS is still nested in the cascade, which affects how specificity is calculated.

You cannot nest at-rules like @media inside selectors in the same way, though the reverse — a selector inside a media query — works fine and is the more common pattern.

Native nesting is Baseline 2023. It is available in all major browsers and safe to use in production without a build step.


The :has() Selector: CSS's Parent Selector

:has() lets you style an element based on its descendants — something CSS has never been able to do before. It's been called the "parent selector" but it's more accurately a relational selector: you can select any element based on what it contains or what follows it.

/* Style a form field when its input is invalid */
.field:has(input:invalid) {
  --field-border-color: var(--color-error);
}

/* Style a card differently when it contains a featured badge */
.card:has(.badge--featured) {
  box-shadow: 0 0 0 2px var(--color-accent);
}

/* Navigation item that is currently active */
nav li:has(> a[aria-current="page"]) {
  background: var(--color-surface-active);
}

The third example — selecting a <li> based on a direct child <a> with a specific attribute — was previously impossible in CSS alone. You'd have needed JavaScript to walk up the DOM and add a class to the <li>.

Real-World Use Cases

:has() is particularly powerful for form state. You can style labels, wrappers, and helper text based on whether the associated input is focused, invalid, or populated — without any JavaScript or class manipulation.

It also enables layout switches based on content. A grid layout that changes when it has fewer than a certain number of children, or a container that adjusts padding when it doesn't contain a certain element type.

:has() is Baseline 2023 and fully available in all modern browsers.


Cascade Layers: Managing Specificity at Scale

Cascade layers (@layer) let you explicitly control which layers of CSS win in the cascade, independent of selector specificity. This solves a structural problem in large codebases: specificity wars between third-party stylesheets, utility classes, component styles, and override styles.

@layer reset, base, components, utilities;

@layer reset {
  *, *::before, *::after { box-sizing: border-box; }
}

@layer components {
  .button { padding: 0.5rem 1rem; }
}

@layer utilities {
  .mt-4 { margin-top: 1rem; }
}

The @layer declaration at the top defines the layer order. Styles in a later-declared layer win over styles in an earlier layer, regardless of specificity. This means a low-specificity utility class in the utilities layer will always beat a high-specificity component rule in the components layer.

This changes how you need to think about CSS architecture. Instead of managing specificity through selector complexity, you manage it through layer order. Third-party library styles can be wrapped in a layer and placed below your own styles:

@import url('third-party.css') layer(vendor);
@layer vendor, base, components, utilities;

Cascade layers are Baseline 2022 and have broad browser support.


CSS Scoping: Component Isolation Without Shadow DOM

@scope lets you apply styles that only affect elements within a defined subtree, with an optional lower boundary:

@scope (.card) to (.card__footer) {
  p {
    font-size: 0.875rem;
  }
}

This rule applies to p elements inside .card but not inside .card__footer or its descendants. The scope is bounded above and below. This is fundamentally different from Shadow DOM encapsulation — scoped styles are still part of the regular cascade, they're just geographically limited.

@scope is particularly useful for component libraries where you want styles to apply within a component boundary without relying on deeply nested selectors or BEM naming conventions. It's also useful for theming zones in a layout — a sidebar might have a scoped dark theme that doesn't bleed into adjacent content.

Browser support for @scope is still maturing. Chrome and Edge have supported it since 2023, Safari since 2024, Firefox from 2025. Check Baseline status before using it without a fallback.


Logical Properties for Internationalization

Physical CSS properties — margin-left, padding-right, border-top — are defined relative to physical screen directions. Logical properties are defined relative to the writing mode: inline (the direction text flows) and block (the direction blocks stack).

/* Physical */
.element {
  margin-left: 1rem;
  padding-top: 0.5rem;
}

/* Logical equivalents */
.element {
  margin-inline-start: 1rem;
  padding-block-start: 0.5rem;
}

In a left-to-right horizontal writing mode, these are equivalent. In a right-to-left language like Arabic or Hebrew, margin-inline-start becomes margin-right automatically, with no JavaScript or RTL stylesheet overrides needed.

The full property set covers margins, paddings, borders, sizing (inline-size instead of width), and positioning (inset-inline-start instead of left). Logical properties are Baseline 2023 and are the correct default for any internationalized application.


Modern Color: oklch, display-p3, and the color() Function

CSS now supports multiple color spaces beyond sRGB. Two are particularly significant for practical use.

OKLCH

OKLCH is a perceptually uniform color space: equal numeric steps produce equal perceived changes in lightness and chroma. This makes it far better than HSL for creating color scales, because an HSL scale with uniform lightness steps will look wildly inconsistent across hues.

:root {
  --color-primary: oklch(55% 0.2 250);
  --color-primary-light: oklch(75% 0.15 250);
  --color-primary-dark: oklch(35% 0.25 250);
}

The three values are lightness (0–100%), chroma (0–0.4+), and hue (0–360). Generating accessible color scales — with guaranteed contrast ratios at defined lightness steps — is much more predictable in OKLCH than in sRGB or HSL.

display-p3

The display-p3 color space covers a wider gamut than sRGB, including colors that sRGB can't represent — particularly vivid greens and reds. On modern displays (most phones, most MacBooks), these colors render as genuinely more vibrant:

.button {
  background: color(display-p3 0.2 0.8 0.3);
}

Use @media (color-gamut: p3) to progressively enhance for wide-gamut displays without affecting sRGB users.


Scroll-Driven Animations Without JavaScript

The Scroll Timeline API lets you drive CSS animations based on scroll position, all in CSS with no JavaScript:

@keyframes reveal {
  from { opacity: 0; transform: translateY(20px); }
  to { opacity: 1; transform: translateY(0); }
}

.card {
  animation: reveal linear both;
  animation-timeline: view();
  animation-range: entry 0% entry 50%;
}

The animation-timeline: view() ties the animation progress to the element's position in the viewport. animation-range defines which part of the scroll range the animation covers. This replaces IntersectionObserver-based reveal animations entirely for the common case.

Scroll-driven animations are Baseline 2024 — Chrome and Edge supported them first, Safari added support in 2024, Firefox in 2025. For production use, check whether your user base has received these browser versions.


Anchor Positioning for Tooltips and Popovers

Anchor positioning solves one of the most painful layout problems: positioning a floating element (tooltip, popover, dropdown) relative to a trigger element, without JavaScript coordinate calculation.

.trigger {
  anchor-name: --my-trigger;
}

.tooltip {
  position: absolute;
  position-anchor: --my-trigger;
  bottom: calc(anchor(top) + 8px);
  left: anchor(center);
  translate: -50% 0;
}

The tooltip is positioned relative to the trigger's edges using the anchor() function. The @position-try at-rule lets you define fallback positions if the primary position would overflow the viewport — which is exactly what made JavaScript-based positioning libraries like Popper.js and Floating UI necessary.

Anchor positioning shipped in Chrome in 2024. Safari and Firefox support is still in progress as of mid-2025. For now, it's appropriate for progressive enhancement or internal tooling, but requires a polyfill or fallback for production apps with broad browser targets.


What Still Requires a Preprocessor

Given everything above, what's left for Sass, PostCSS, or Less?

Still valid reasons for a preprocessor:

  • Mixins and functions. CSS custom properties cover a lot of the variable use case, but Sass mixins that generate multiple declarations from parameters have no direct CSS equivalent yet.
  • Compile-time color manipulation. OKLCH makes runtime color generation more predictable, but if you need to generate a full color scale at build time from a single token, Sass or a PostCSS plugin still does this better.
  • Legacy browser support. PostCSS with preset-env can transpile modern CSS features for older browsers. If you have IE11 or early Chrome requirements (unlikely but possible in enterprise contexts), you still need a build step.
  • CSS Modules (technically PostCSS). If you're using CSS Modules for scoped class names in a component system, that's a PostCSS/webpack feature, not a preprocessor per se — but it requires a build step.

No longer valid reasons:

  • Nesting (native)
  • Variables (custom properties)
  • Basic @import consolidation (use @layer + native imports)
  • Most pseudo-class logic (:has() covers a huge amount of what &:not() combinator chains tried to do)

The trajectory is clear: each CSS release cycle narrows the gap further.


The Browser Support Landscape

The Baseline framework, introduced by the W3C Web Platform group, categorizes feature support into two tiers:

Baseline Newly Available: Supported in all major browser engines but not yet widely deployed (within the past year).

Baseline Widely Available: Supported for more than 30 months across all major engines — safe to use without fallbacks for most audiences.

Features available as Baseline Widely Available as of 2025:

  • Container queries (inline-size)
  • CSS nesting
  • :has() selector
  • Cascade layers
  • CSS Grid subgrid
  • Logical properties
  • Color functions (oklch, display-p3 for color())

Features at Baseline Newly Available or still in progress:

  • Scroll-driven animations (Baseline 2024, still maturing in Firefox/Safari adoption)
  • Anchor positioning (Chrome-leading, Firefox/Safari in progress)
  • @scope (Baseline 2024, Firefox shipped 2025)
  • Style queries (shipping but less consistent)

Check caniuse.com and web.dev/baseline for current status before using any newer feature in production. The landscape shifts every quarter.

The bottom line: modern CSS is a genuinely capable language. The developers still reflexively reaching for a JavaScript library to handle a positioning problem or a Sass mixin to manage a color scale are working harder than they need to. Audit your assumptions — a lot of what required workarounds two years ago is now solved by the platform.