CSS has changed more in the last few years than in the decade before. Features that used to need JavaScript, preprocessors or clever hacks are now built into every modern browser.
Here are eight you can use in production today.
1. Container queries
Media queries respond to the screen size. Container queries respond to the size of the parent element — perfect for reusable components that might sit in a wide main column or a narrow sidebar.
.card-wrapper {
container-type: inline-size;
}
@container (min-width: 480px) {
.card {
display: grid;
grid-template-columns: 200px 1fr;
}
}
The same card now lays itself out correctly wherever you drop it.
2. The :has() selector
For twenty years developers asked for a “parent selector”. Now we have one — and it’s even more powerful.
/* Style a card differently if it contains an image */
.card:has(img) { padding: 0; }
/* Highlight a form field's label when the input is invalid */
label:has(+ input:invalid) { color: crimson; }
/* Dim the page when a dialog is open */
body:has(dialog[open]) { overflow: hidden; }
3. Native nesting
You no longer need Sass just to nest selectors:
.nav {
display: flex;
gap: 1rem;
& a {
color: inherit;
&:hover { color: var(--accent); }
}
@media (width < 640px) {
flex-direction: column;
}
}
Notice the width < 640px syntax? That’s the new media query range syntax — much easier to read than max-width: 639px.
4. color-mix()
Create tints, shades and transparent versions of a brand colour without hard-coding a dozen hex values:
:root { --brand: #6366f1; }
.badge {
background: color-mix(in srgb, var(--brand) 12%, transparent);
border: 1px solid color-mix(in srgb, var(--brand) 30%, white);
color: var(--brand);
}
Change --brand once and every derived colour updates automatically. (This very blog’s theme uses this trick for its category badges.)
5. Cascade layers (@layer)
Specificity wars are over. Layers let you decide which group of styles wins, regardless of selector strength:
@layer reset, base, components, utilities;
@layer components {
.button { padding: 12px 20px; }
}
@layer utilities {
.p-0 { padding: 0; } /* always wins over components */
}
6. View transitions
Smooth, app-like animations between page states — or even between full page loads — with almost no code:
@view-transition { navigation: auto; }
.post-title { view-transition-name: post-title; }
Add that to a multi-page site and navigating between pages cross-fades automatically, with matching elements morphing into place.
7. Subgrid
Nested grids can finally align to their parent’s tracks. Card titles, text and buttons line up perfectly across a row, even when content lengths differ:
.cards { display: grid; grid-template-columns: repeat(3, 1fr); }
.card {
display: grid;
grid-row: span 3;
grid-template-rows: subgrid;
}
8. text-wrap: balance and pretty
Two tiny properties that make typography look professionally set:
h1, h2, h3 { text-wrap: balance; } /* evenly balanced headline lines */
p { text-wrap: pretty; } /* avoids a lonely last word */
Browser support at a glance
| Feature | Chrome / Edge | Firefox | Safari |
|---|---|---|---|
| Container queries | ✅ | ✅ | ✅ |
| :has() | ✅ | ✅ | ✅ |
| Native nesting | ✅ | ✅ | ✅ |
| color-mix() | ✅ | ✅ | ✅ |
| Cascade layers | ✅ | ✅ | ✅ |
| View transitions (same-document) | ✅ | ✅ | ✅ |
Always check caniuse.com for the latest details on newer sub-features before relying on them.
Final thoughts
Modern CSS lets you write less code, ship less JavaScript and build components that adapt to any layout. Pick one feature from this list — :has() is a great start — and refactor a single component this week. You won’t want to go back.
The :has() examples are gold. Just deleted 60 lines of JavaScript from a project.
Didn’t know nesting was native now. Time to drop Sass on my next project!