Skip to content

Lesson 6 of 9 · 20m

Modern CSS: Custom Properties, Container Queries, and :has()

The CSS that shipped while everyone was using frameworks — design tokens, container queries, and the parent selector we waited twenty years for.

The CSS you learned five years ago is obsolete — in a good way. Features that required JavaScript or Sass now ship in every browser: custom properties for runtime theming, container queries for component-level responsiveness, and :has() — the parent selector the web waited twenty years for.

Custom properties are runtime state

:root {
  --surface: #0a0a0a;
  --text: #ededed;
}
.card { background: var(--surface); color: var(--text); }

Unlike Sass variables, these are real values in the cascade — they inherit, they update live, and JavaScript can write them: el.style.setProperty("--surface", "#fff"). That is how every serious dark-mode toggle works: one attribute flip on <html>, every component follows.

Container queries: components that fit anywhere

Media queries answer "how wide is the viewport?" — useless for a component that lives in a sidebar or a full-width column. Container queries ask the right question:

.card-list { container-type: inline-size; }
@container (min-width: 480px) {
  .card { display: grid; grid-template-columns: 160px 1fr; }
}

The card responds to the space it actually gets. This is the primitive design systems were built to fake for a decade.

:has() — CSS can finally look backwards

form:has(input:invalid) .submit { opacity: 0.5; }
.card:has(img) { padding: 0; }
tr:has(td.overdue) { color: var(--red); }

Highlight a form with an invalid field, style a card that contains an image, flag a table row by its content — all things teams wrote JavaScript observers for. Browser support is now universal; the excuse is gone.

Field notes

  • Cascade layers (@layer) solve specificity wars — put resets in @layer base and your components stop fighting resets with !important.
  • color-mix() generates tints without Sass: color-mix(in oklab, var(--accent) 20%, transparent).
  • None of this needs a build step. The build step was polyfilling a missing platform — the platform arrived.