For a long time, "modern CSS" meant Grid, Flexbox, and custom properties. Those features are still doing plenty of work, but the platform has moved again. Components can respond to their own size. Selectors can react to descendant state. The cascade can be ordered deliberately. Several jobs that used to need a preprocessor or a small JavaScript helper now belong in CSS.

That does not mean every new property belongs in production. A feature earns its place when it simplifies real code and fits the browsers your users actually run. The eight features below clear that bar in many projects today.

Start with browser support, not novelty

Support tables are easy to skim and easy to misuse. A global percentage cannot tell you which browsers your customers use, whether a feature fails safely, or how expensive a fallback would be. Check your own analytics and define a browser policy before changing code.

Web Platform Baseline is a good first filter. "Newly available" means the feature works across the current Baseline browser set. "Widely available" means that cross-browser support has had more time to settle. Neither label replaces testing, but both are more useful than memorizing version numbers.

A useful default: ship a supported feature when the fallback remains usable. Be more cautious when lack of support would hide content, block input, or break navigation.

Container queries make components genuinely reusable

Media queries know the viewport. Components often need to know the space they received. A card may sit in a wide results grid, a narrow sidebar, or a full-width feature slot at the same viewport width. Container queries let the card respond to that local space.

Viewport workaround

@media (width > 60rem) {
  .card {
    grid-template-columns: 12rem 1fr;
  }
}

Component rule

.card-shell {
  container-type: inline-size;
}

@container (width > 34rem) {
  .card {
    grid-template-columns: 12rem 1fr;
  }
}

The component no longer needs to know where it lives. MDN's container query guide covers named containers, query units, and fallbacks.

:has() removes mirrored state classes

A common script listens for a change, walks up the DOM, and toggles a class on the parent so CSS can style it. If that class only mirrors descendant state, :has() can often express the relationship directly.

.field:has(input:invalid:not(:placeholder-shown)) {
  border-color: var(--danger);
}

.filter-panel:has(input:checked) .filter-panel__clear {
  display: inline-flex;
}

Use it for a real relationship, not as a reason to write selectors that crawl through half the document. Keep the selector close to the component boundary and watch specificity. If support is uncertain, MDN documents how to test selector support with @supports selector().

Native nesting removes preprocessor-only syntax

Native CSS nesting keeps related selectors together without requiring Sass for that job alone. It is especially good for component states and media queries. It is less good when nesting becomes a five-level map of the DOM.

.notice {
  padding: 1rem;

  & strong {
    color: var(--accent-deep);
  }

  &:has(a:focus-visible) {
    outline: 3px solid var(--focus);
  }

  @media (width > 48rem) {
    padding: 1.5rem;
  }
}

The nesting selector is widely available. The bigger migration question is whether removing a build dependency is worth the churn. If Sass also provides functions, modules, or generated utilities you rely on, nesting alone is not a reason to rewrite the stack.

Cascade layers make precedence explicit

Specificity fights often begin because source order is doing several jobs at once. Cascade layers let you set the order of broad groups, then keep selectors inside those groups relatively simple.

@layer reset, vendor, components, utilities;

@import url("vendor.css") layer(vendor);

@layer components {
  .button {
    background: var(--accent);
  }
}

@layer utilities {
  .visually-hidden {
    position: absolute;
    clip-path: inset(50%);
  }
}

This is not a license to create dozens of layers. A short, declared order is easy to understand. An elaborate layer taxonomy can be as difficult as the specificity problem it replaced. The @layer reference includes import and nesting behavior.

Logical properties remove directional assumptions

Physical properties describe a screen edge. Logical properties describe the flow of the content. Replacing paired left and right declarations with an inline shorthand makes CSS shorter and lets the same component work in right-to-left or vertical writing modes.

Physical directions

.banner {
  padding-left: 1.5rem;
  padding-right: 1.5rem;
  margin-top: 2rem;
}

Content flow

.banner {
  padding-inline: 1.5rem;
  margin-block-start: 2rem;
}

You do not need to convert every property in one pull request. Start with shared components and paired spacing declarations. MDN has a complete logical properties guide.

light-dark() keeps theme tokens together

A theme often duplicates the same token names across two selectors. With color-scheme and light-dark(), each semantic token can carry both values in one place.

:root {
  color-scheme: light dark;
  --paper: light-dark(#fff8f0, #141814);
  --ink: light-dark(#171717, #eef6ef);
  --accent: light-dark(#367d59, #82c99f);
}

body {
  color: var(--ink);
  background: var(--paper);
}

That is the exact pattern CSS Radar uses. A manual toggle can still set color-scheme on the root. The improvement is that JavaScript persists a preference instead of owning the palette.

@starting-style handles entry transitions

Elements that begin at display: none, including dialogs and popovers, used to need a forced layout or an extra class before their entry transition could run. @starting-style gives the browser the starting values directly.

dialog {
  opacity: 0;
  translate: 0 0.75rem;
  transition:
    opacity 200ms,
    translate 200ms,
    display 200ms allow-discrete;
}

dialog[open] {
  opacity: 1;
  translate: 0 0;
}

@starting-style {
  dialog[open] {
    opacity: 0;
    translate: 0 0.75rem;
  }
}

Treat this as progressive enhancement. The dialog should still open and close when the transition is absent. Motion should also stop under prefers-reduced-motion: reduce.

What should you change first?

Start with replacements that are easy to prove. aspect-ratio can replace a percentage-padding box. Logical shorthands can replace paired physical spacing. Container queries can remove a component's dependency on the page viewport. These changes are small enough to review and test in isolation.

The more behavioral the feature, the more evidence you need. Before deleting a parent state class or changing how an entry animation starts, check keyboard behavior, reduced motion, analytics hooks, and the oldest browser you support. Modern CSS is not better because it is newer. It is better when the browser can do the same job with less code for your team to maintain.

Primary references