Remember when CSS was just about changing colors and adding margins? Those days feel like ancient history now. CSS has blossomed into something truly remarkable - a design language that understands context, relationships, and even motion. And honestly? I couldn't be more excited about where we are in 2026.
Today, I want to share some of my favorite modern CSS features with you. Not just the syntax (though we'll cover that), but the why behind each one. Because great CSS isn't just about making things look pretty - it's about creating experiences that feel intuitive and work for everyone.
Let's dive in together.
Container Queries: Components That Understand Their World
For years, we've used media queries to create responsive designs. They work, but they have a fundamental limitation: they only know about the viewport, not the container a component lives in.
Think of it like this: media queries are like asking "How big is the room?" when what you really need to know is "How big is this shelf I'm sitting on?"
Container queries change everything. They let components respond to their immediate environment, not just the window size.
/* First, establish a containment context */ .card-container { container-type: inline-size; container-name: card; } /* Now the card can respond to its container's width */ .card { display: grid; gap: 1rem; padding: 1rem; } /* When the container is wide enough, switch to horizontal layout */ @container card (min-width: 400px) { .card { grid-template-columns: 200px 1fr; } } @container card (min-width: 600px) { .card { grid-template-columns: 250px 1fr; padding: 1.5rem; } .card__title { font-size: 1.5rem; } }
Why does this matter for your users? Imagine a card component used in a main content area and a narrow sidebar. With media queries, you'd need to write specific styles knowing where each instance lives. With container queries, the card just... figures it out. It adapts based on the space it has, regardless of where you put it.
This is a philosophical shift. We're moving from "pages that respond to screens" to "components that respond to context." Your design system becomes more modular, more reusable, and frankly, more intelligent.
Container Query Units
Here's a delightful bonus - container query units let you size things relative to the container:
.card-container { container-type: inline-size; } .card__title { /* Size text relative to container width */ font-size: clamp(1rem, 5cqi, 2rem); } .card__image { /* 30% of container's inline size */ width: 30cqi; }
The units cqi (container query inline) and cqb (container query block) open up possibilities we've only dreamed about before.
The :has() Selector: CSS Can Finally Look Up the Family Tree
For the longest time, CSS could only style downward. You could select children, descendants, siblings that come after - but never parents or previous siblings. It felt like having a conversation where you could only talk about what's ahead of you, never what's behind.
The :has() selector changes that completely. Some call it the "parent selector," but it's so much more. It's a relational selector that lets you style elements based on what they contain or what comes after them.
/* Style a card differently when it contains an image */ .card:has(img) { display: grid; grid-template-rows: auto 1fr; } /* Style a card without an image */ .card:not(:has(img)) { padding-top: 2rem; } /* Highlight a form group when its input is focused */ .form-group:has(input:focus) { background-color: var(--highlight-subtle); border-left: 3px solid var(--primary-color); } /* Style a label when its associated input is invalid */ .form-group:has(input:invalid) label { color: var(--error-color); }
This is genuinely transformative for accessibility. Think about form validation - you can now style parent containers, labels, and helper text based on the state of form inputs. No JavaScript required.
/* A complete form validation styling system */ .field-wrapper:has(input:valid) .success-icon { opacity: 1; } .field-wrapper:has(input:invalid:not(:placeholder-shown)) .error-message { display: block; } .field-wrapper:has(input:required) label::after { content: " *"; color: var(--error-color); }
One of my favorite uses is for quantity-based styling:
/* Style a navigation differently based on number of items */ nav:has(> a:nth-child(6)) { /* More than 5 items? Switch to a different layout */ flex-direction: column; } /* Style an image gallery based on image count */ .gallery:has(img:only-child) { /* Single image gets special treatment */ display: block; } .gallery:has(img:nth-child(2)):not(:has(img:nth-child(3))) { /* Exactly two images */ grid-template-columns: 1fr 1fr; }
Subgrid: True Alignment Across Nested Components
Grid layout was revolutionary, but it had a limitation: nested grids couldn't participate in their parent's grid. If you had a card with a header, body, and footer, aligning those elements across multiple cards required clever hacks.
Subgrid solves this elegantly. Nested elements can inherit their parent's grid tracks, creating true alignment across siblings.
.card-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 2rem; } .card { display: grid; /* Inherit rows from parent? No - define structure for subgrid */ grid-template-rows: auto 1fr auto; gap: 1rem; } /* When cards are in a grid context, use subgrid */ .card-grid .card { grid-row: span 3; } .card-grid .card > * { /* Let content areas participate in parent's alignment */ }
Actually, let me show you a more practical example - a pricing table where everything aligns beautifully:
.pricing-grid { display: grid; grid-template-columns: repeat(3, 1fr); grid-template-rows: auto auto 1fr auto auto; gap: 0 2rem; } .pricing-plan { display: grid; grid-template-rows: subgrid; grid-row: span 5; padding: 2rem; border: 1px solid var(--border-color); border-radius: 1rem; } .pricing-plan h3 { /* Plan name - row 1 */ } .pricing-plan .price { /* Price - row 2 */ } .pricing-plan .features { /* Features - row 3, flexible */ } .pricing-plan .cta { /* Call to action - row 4 */ } .pricing-plan .guarantee { /* Guarantee text - row 5 */ }
Now your "Basic," "Pro," and "Enterprise" plans will have perfectly aligned elements, even if one has more features than the others. The feature lists can grow without throwing off the alignment of CTAs below them.
For your users, this means cleaner, more scannable interfaces. When things align properly, our brains process information more easily. It's a small thing that makes a big difference.
color-mix(): Harmonious Color Relationships
Creating consistent color systems has always been tricky. How do you make a hover state that's slightly darker? A disabled state that's more muted? Usually, we'd define these manually or use preprocessors.
color-mix() lets you blend colors directly in CSS, creating dynamic relationships that stay harmonious:
:root { --primary: #3b82f6; --on-primary: white; } .button { background-color: var(--primary); color: var(--on-primary); } .button:hover { /* Mix primary with black for a darker shade */ background-color: color-mix(in srgb, var(--primary) 85%, black); } .button:active { background-color: color-mix(in srgb, var(--primary) 70%, black); } .button:disabled { /* Mix with gray to desaturate */ background-color: color-mix(in srgb, var(--primary) 50%, gray); opacity: 0.7; }
What makes this special is the color space parameter. Different color spaces produce different blending results:
/* sRGB blending - what you might expect */ .example-srgb { background: color-mix(in srgb, blue 50%, yellow); } /* oklch blending - perceptually uniform, often more pleasing */ .example-oklch { background: color-mix(in oklch, blue 50%, yellow); } /* Create accessible focus rings that work with any background */ .interactive:focus-visible { outline: 3px solid color-mix(in oklch, var(--primary) 80%, currentColor); outline-offset: 2px; }
I love using color-mix() for theming systems:
:root { --surface: white; --text: black; } .card { background: var(--surface); color: var(--text); /* Subtle border that works in any theme */ border: 1px solid color-mix(in srgb, var(--text) 15%, transparent); } .card-elevated { /* Shadow color derived from text color */ box-shadow: 0 4px 12px color-mix(in srgb, var(--text) 10%, transparent); }
View Transitions: Smooth, Meaningful Motion
Motion isn't just decoration - it helps users understand what's happening in an interface. When done well, transitions guide attention and provide continuity. When done poorly (or not at all), interfaces feel jarring and disorienting.
The View Transitions API, now fully supported, makes smooth page transitions almost trivially easy:
/* Enable view transitions for your document */ @view-transition { navigation: auto; } /* Customize the default crossfade */ ::view-transition-old(root), ::view-transition-new(root) { animation-duration: 0.3s; animation-timing-function: ease-out; } /* Give specific elements their own transitions */ .hero-image { view-transition-name: hero; } .page-title { view-transition-name: title; } /* Customize how these elements transition */ ::view-transition-old(hero), ::view-transition-new(hero) { animation-duration: 0.4s; } ::view-transition-old(title) { animation: slide-out-up 0.3s ease-in; } ::view-transition-new(title) { animation: slide-in-down 0.3s ease-out; }
The magic happens when navigating between pages. If both pages have an element with view-transition-name: hero, the browser smoothly morphs one into the other. It's like the shared element transitions you see in native mobile apps, but in CSS.
For accessibility, always respect user preferences:
@media (prefers-reduced-motion: reduce) { ::view-transition-group(*), ::view-transition-old(*), ::view-transition-new(*) { animation: none !important; } }
Scroll-Driven Animations: Motion That Responds to Users
This one fills me with joy. Scroll-driven animations let you tie animation progress to scroll position - no JavaScript scroll listeners needed (and no janky performance either).
@keyframes fade-in-up { from { opacity: 0; transform: translateY(50px); } to { opacity: 1; transform: translateY(0); } } .reveal-on-scroll { animation: fade-in-up linear both; animation-timeline: view(); animation-range: entry 0% entry 100%; }
The animation-timeline: view() tells the browser to drive this animation based on when the element enters the viewport. animation-range controls exactly when the animation starts and ends.
Let's build something more interesting - a reading progress indicator:
.progress-bar { position: fixed; top: 0; left: 0; width: 100%; height: 4px; background: var(--primary); transform-origin: left; animation: grow-width linear; animation-timeline: scroll(root block); } @keyframes grow-width { from { transform: scaleX(0); } to { transform: scaleX(1); } }
That's it. A pure CSS reading progress bar that smoothly fills as you scroll through the page. No JavaScript, no scroll event listeners, no performance concerns.
Here's a parallax effect that doesn't make you reach for a library:
.parallax-container { overflow: hidden; } .parallax-bg { animation: parallax linear; animation-timeline: scroll(root); } @keyframes parallax { from { transform: translateY(-20%); } to { transform: translateY(20%); } }
Bringing It All Together
These features aren't just technical improvements - they represent a shift in what CSS can express. We can now build interfaces that:
- Understand context through container queries
- Respond to relationships with :has()
- Maintain alignment across complex layouts with subgrid
- Generate harmonious colors dynamically with color-mix()
- Transition smoothly between states with view transitions
- React to user scrolling with scroll-driven animations
Here's a component that uses several of these features together:
.feature-card { container-type: inline-size; view-transition-name: var(--card-id); background: var(--surface); border: 1px solid color-mix(in srgb, var(--text) 10%, transparent); border-radius: 1rem; animation: reveal linear both; animation-timeline: view(); animation-range: entry 0% entry 50%; } .feature-card:has(.badge--new) { border-color: color-mix(in oklch, var(--accent) 50%, transparent); } @container (min-width: 400px) { .feature-card { display: grid; grid-template-columns: auto 1fr; gap: 1.5rem; } } @keyframes reveal { from { opacity: 0; transform: translateY(20px); } }
A Note on Progressive Enhancement
Not every browser supports every feature equally. And that's okay. CSS is beautifully forgiving - browsers simply ignore properties they don't understand.
Use @supports to provide fallbacks:
.card { display: flex; flex-direction: column; } @supports (container-type: inline-size) { .card-wrapper { container-type: inline-size; } @container (min-width: 400px) { .card { flex-direction: row; } } }
Moving Forward with Empathy
As I wrap up, I want to leave you with a thought: every CSS feature we've discussed exists to serve people. Container queries help users on any device. The :has() selector enables better form feedback. Scroll-driven animations create delight without sacrificing performance.
When you're building with these tools, keep your users at the center. Test with screen readers. Respect motion preferences. Consider slow connections and older devices.
CSS in 2026 gives us incredible power to create beautiful, responsive, accessible experiences. Let's use that power thoughtfully.
I'd love to hear what you build with these features. The web is better when we share what we learn.
Happy styling!
