Modern CSS
: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
/* 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.