Flexbox and Grid are table stakes now. The newer CSS features build on them in ways that solve real layout problems we’ve been hacking around for years.
Container Queries
Media queries ask “how wide is the viewport?” Container queries ask “how wide is my parent?” This is the feature component-based design has been waiting for.
.card-container {
container-type: inline-size;
container-name: card;
}
@container card (min-width: 400px) {
.card {
display: grid;
grid-template-columns: 200px 1fr;
gap: 1rem;
}
}
@container card (max-width: 399px) {
.card {
display: flex;
flex-direction: column;
}
}
Now your card component adapts to where it’s placed — sidebar, main content, full-width — without the parent needing to know about it.
Subgrid
Grid’s biggest limitation was that children couldn’t participate in a parent’s grid. Subgrid fixes this:
.product-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1.5rem;
}
.product-card {
display: grid;
grid-template-rows: subgrid;
grid-row: span 3; /* image, title, price */
}
Every card’s title aligns with every other card’s title, regardless of content length. No more equal-height hacks.
has() Selector
The parent selector CSS never had:
/* Style a form group that contains an invalid input */
.form-group:has(input:invalid) {
border-color: red;
}
/* Style a card differently when it contains an image */
.card:has(img) {
grid-template-rows: 200px 1fr;
}
/* Navigation with dropdown open */
nav:has(.dropdown:hover) {
background: var(--color-surface);
}
This eliminates entire categories of JavaScript that existed only to toggle parent classes.
Scroll-Driven Animations
Animate elements based on scroll position, entirely in CSS:
.progress-bar {
animation: grow linear;
animation-timeline: scroll();
}
@keyframes grow {
from { transform: scaleX(0); }
to { transform: scaleX(1); }
}
No Intersection Observer. No scroll event listeners. No JavaScript at all.
Logical Properties
Write layout that works in any writing direction:
.sidebar {
margin-inline-start: 1rem;
padding-block: 2rem;
border-inline-end: 1px solid var(--border);
}
margin-inline-start is margin-left in LTR and margin-right in RTL. If your site supports internationalization, use logical properties everywhere.
text-wrap: balance and pretty
h1 { text-wrap: balance; }
p { text-wrap: pretty; }
balance distributes text evenly across lines (great for headings). pretty avoids orphans on the last line. Two lines of CSS that replace a lot of manual line-break management.
CSS has gotten genuinely powerful. The days of needing JavaScript for basic layout interactions are ending.