Modern CSS

The last few years added capabilities that used to require JavaScript or preprocessor build steps: nesting, container queries, parent selection with :has(), typed custom properties, and perceptual color. This lesson tours them — with the shipping discipline that keeps modern features from breaking older browsers.

Native nesting

Nest rules without a preprocessor

/* Native nesting: supported in every evergreen browser.
   The nested selector needs & (unlike Sass, implicit & is NOT assumed). */
.card {
  padding: 1rem;

  & > .card__title {          /* explicit child reference */
    font-weight: 600;
  }

  &:hover {                   /* state variants live with the component */
    border-color: var(--color-action);
  }

  @media (min-width: 48em) {  /* media queries nest too */
    padding: 2rem;
  }
}

Nesting trades grep-ability for locality: everything about a component sits in one block. Whether that trade is worth it depends on the codebase — teams using nesting usually pair it with a naming convention (see Architecture) so styles remain findable.

Container queries

Components that respond to their own size

Comparison of media queries measuring the viewport and container queries measuring a component box
Media queries measure the viewport; container queries measure the element's container. The same card can now reflow itself depending on the slot it lands in.
/* 1. Opt IN on the ancestor: this element becomes queryable */
.card-slot { container-type: inline-size; }

/* 2. Query it: the card reflows by its own measured width */
.card {
  display: grid;
  gap: 0.75rem;
}

@container (min-width: 24rem) {
  .card {
    grid-template-columns: 6rem 1fr;  /* wide slot: media object layout */
  }
}

This is the change that retires most "card layout" media queries: a component in a narrow sidebar and the same component in a wide main column now style themselves correctly, regardless of viewport.

:has() and the new selectors

Styling the parent at last

/* :has() selects an element BY ITS CONTENTS - the "parent selector" */
.card:has(img) { padding: 0; }                 /* only cards with an image */
.form-field:has(input:invalid) label { color: #ef4444; }
.grid:has(.featured) { grid-template-columns: 1fr 1fr; }  /* container reacts */

/* Quantity queries: style depending on HOW MANY siblings exist */
ul li:nth-last-child(n + 5):first-child { font-size: 0.875rem; }
/* "a first child with at least 4 more siblings after it" -> tighten spacing */

:has() moves decisions from JavaScript to the stylesheet — but it also couples a rule to markup you cannot see from the rule's location. Comment the markup contract when you use it.

More capabilities worth knowing

Logical properties and perceptual color

/* Writing-mode aware spacing (Box Model lesson) */
.card { padding-inline: 1rem; margin-block-start: 1.5rem; }

/* Perceptual color spaces and relative color syntax */
.brand { color: oklch(0.62 0.19 260); }
.brand-dark { color: oklch(from oklch(0.62 0.19 260) calc(l - 0.1) c h); }

Typed variables, view transitions, and anchors

/* @property: give a custom property a type so it can transition (Variables lesson) */
@property --panel-x { syntax: "<length>"; inherits: false; initial-value: 0px; }

/* scroll-driven animations: progress driven by scroll position, no JavaScript */
@keyframes reveal { from { opacity: 0; } to { opacity: 1; } }
.reveal { animation: reveal linear both; animation-timeline: view(); }

Verify support before relying on any of these: the cssdb database (linked from References) tracks each feature's standardization stage and browser support.

Shipping modern CSS safely

The fallback-first pattern

/* Declare the fallback FIRST, the modern value after.
   Invalid-at-parse-time values are dropped (Syntax lesson), so old
   browsers keep the first, new browsers the second. */
.gallery {
  display: flex;           /* fallback: usable everywhere */
  flex-wrap: wrap;
  display: grid;           /* modern: better alignment when supported */
  grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
}

/* @supports gates behavior that cannot degrade gracefully */
@supports not (container-type: inline-size) {
  /* JS-free alternatives or wider defaults for old browsers */
}

The support matrix is a product decision

"Modern" is not "experimental": nesting, :has(), container queries, and OKLCH are shipped across evergreen browsers; anchor positioning is not. Check caniuse.com per feature, decide your baseline, and record it — the feature list is the easy part.

Practice: modernize a component

The task

Take the card component from the Flexbox practice and rebuild it with modern primitives, then compare the two stylesheets line by line.

The checklist

  • Convert a card's media queries to container queries and verify it in a narrow sidebar.
  • Replace one JavaScript-free "parent gets a class" pattern with :has().
  • Nest one component's styles and confirm the dev-tools Styles pane shows the resolved selectors.
  • Add fallbacks for every modern declaration you used and test with @supports not.