CSS in 2026: What’s Actually Worth Using in Production

In 2026 I write plain modern CSS: container queries for component layout, :has() for parent styling, nesting, custom properties, and clamp() for fluid type. Skip preprocessors, most utility frameworks, and polyfills. Everything here ships in current Chrome, Safari, Firefox, and Edge, so no build step is needed.
Part of the complete web design guide.
On this page
- Container queries replaced most of my media queries
- :has() is the parent selector we waited twenty years for
- Nesting is native now, and the syntax matters
- @scope solves the leaky-component problem
- Color and units: the small wins add up
- What I still skip
- How this changes a real project
- FAQ
- Do I still need Sass or Less in 2026?
- Is it safe to use :has() and container queries on a site with older visitors?
- What should I learn first if I’m behind?
CSS has changed more in the last four years than in the previous fifteen. Most of what used to require a preprocessor, a JavaScript library, or a polyfill now ships in every browser your customers actually use. Here’s what I reach for on real projects in 2026, and what I still leave on the shelf.
I write CSS for client sites and internal tools, mostly small teams that don’t have time to babysit a build pipeline. That bias shows below. Everything here works in current Chrome, Safari, Firefox, and Edge without a transpiler.
Container queries replaced most of my media queries
Media queries ask “how wide is the viewport?” Container queries ask “how wide is the box this component lives in?” That’s almost always the question you actually meant. A card in a three-column grid and the same card in a sidebar are different layouts, and the viewport width doesn’t tell you which one you’re rendering.
.card-wrap {
container-type: inline-size;
container-name: card;
}
.card {
display: grid;
gap: 1rem;
}
@container card (min-width: 30rem) {
.card {
grid-template-columns: 12rem 1fr;
align-items: start;
}
}
Once you start thinking in containers, component CSS gets shorter. You stop writing breakpoints per page and start writing them per component, which means the same card works in a blog post, a dashboard widget, and a modal without three variants.
One gotcha: container-type: inline-size establishes containment, which can affect how the element sizes in a flex or grid context. If a component suddenly collapses, that’s usually why.
:has() is the parent selector we waited twenty years for
I use :has() constantly now. It’s fully supported across modern browsers, and it removes a whole category of JavaScript that existed purely to add a class when a child changed state.
/* Label the field when the input is invalid */
.field:has(input:user-invalid) {
border-color: #c0392b;
}
.field:has(input:user-invalid) .hint {
display: block;
}
/* Card with an image gets different padding */
.card:has(> img) {
padding: 0;
}
/* Only space the stack if there's more than one child */
.stack:has(> * + *) {
gap: 1.5rem;
}
Note :user-invalid rather than :invalid. It only fires after the user has actually interacted with the field, so you’re not yelling at someone for an empty input they haven’t reached yet. Small thing, huge difference in how forms feel.
Nesting is native now, and the syntax matters
Native CSS nesting shipped and stabilized. The important detail most people get wrong: if you nest a bare element selector, it’s interpreted as a descendant, not as a compound selector with the parent. Use & when you mean “this element.”
.nav {
display: flex;
gap: 0.5rem;
& a {
color: inherit;
text-decoration: none;
padding: 0.5rem 0.75rem;
}
& a:hover,
& a:focus-visible {
background: color-mix(in oklab, currentColor 12%, transparent);
border-radius: 6px;
}
/* Not the same as writing 'a' above */
a { color: red; } /* this is .nav a, fine, but be explicit */
}
You don’t need Sass for nesting anymore. You may still want a build step for other reasons, but “I need nesting” stopped being one of them a while ago.
@scope solves the leaky-component problem
For years the answer to “how do I stop this stylesheet bleeding into that section” was BEM, CSS Modules, or a utility framework. @scope is the platform’s answer, and it’s cleaner than all three for a lot of cases.
@scope (.prose) to (.prose__escape) {
p { margin-block: 0 1.25em; }
h2 { margin-block: 2.5em 0.75em; }
a { text-underline-offset: 3px; }
}
The to clause sets a boundary. Anything inside .prose__escape is exempt, which is exactly what you want for embedded code blocks, third-party widgets, or a CMS field that dumps raw HTML into your carefully styled article.
Color and units: the small wins add up
Two things I use on every project now.
color-mix() lets you derive hover states, borders, and tints from a single brand color instead of hardcoding twelve hex values that drift apart over time.
:root {
--brand: oklch(58% 0.17 250);
--brand-hover: color-mix(in oklab, var(--brand) 85%, black);
--brand-tint: color-mix(in oklab, var(--brand) 12%, white);
}
Logical properties (margin-block, padding-inline, inset-inline-start) mean your layout doesn’t break when someone switches to a right-to-left language. If you’re building anything that might get translated, and most business sites eventually do, this is free insurance.
What I still skip
- Anchor positioning. Genuinely useful for tooltips and dropdowns, but support is still uneven enough that I’d need a fallback. Not worth the double implementation yet on client work.
- View transitions for cross-document navigation. Beautiful when it works, but the fallback experience varies and it’s easy to make a site feel slower. I use it for small in-page state changes only.
- Any layout that requires
subgriddeep in a tree. Support is fine now, but debugging nested subgrid is still unpleasant. I’ll use it one level deep and stop there. - New CSS frameworks. The language caught up. I’d rather ship 200 lines of CSS I understand than adopt a dependency I’ll be migrating off in three years.
How this changes a real project
The practical effect is fewer dependencies and less JavaScript. A typical marketing site I build now has no Sass, no utility framework, and maybe a few dozen lines of JS for menus and form validation, because :has(), :user-invalid, and details/summary handle the rest. Fewer moving parts means fewer things to break at 2am.
If you’re maintaining an older site and wondering whether a rewrite is worth it, the honest answer is usually no. You can adopt container queries and :has() incrementally, in one component at a time, without touching the rest. That’s how I’d approach it, whether the site belongs to a local business here on Long Island or a company with a full design system team. For smaller sites where this stuff matters less than just getting the thing built, the tradeoffs look different, and that’s a conversation worth having before anyone opens an editor. If you want a second opinion on your own setup, I do Long Island web design and modernization work and I’m happy to look at what you’ve got.
FAQ
Do I still need Sass or Less in 2026?
For most projects, no. Nesting, variables, and color functions are native now. You’d only keep a preprocessor for legacy code that already depends on its mixin libraries, or for build-time features like loops over large token sets.
Is it safe to use :has() and container queries on a site with older visitors?
Yes. Both are supported in all current major browsers, and older browsers simply ignore the rules, so you get your base layout without the enhancement. That’s a graceful failure, not a broken page. Test with your actual analytics, not assumptions.
What should I learn first if I’m behind?
Container queries, then :has(). Those two change how you structure components more than anything else on this list, and you can start using both this week without a build step or a browser-support spreadsheet.


