CSS in 2026: You Might Not Need That Library Anymore
I started building websites when CSS floats were the way you made columns. Then I discovered Sass and felt like I'd found religion. Then I moved to CSS-in-JS and spent two years writing const StyledButton = styled.button like it was a perfectly normal thing to do. Then Tailwind came along, and I went full utility-class for a while.
Now, in 2026, I'm watching native CSS quietly absorb most of the reasons I ever reached for those tools. And I find it genuinely surprising how far things have come.
This post is about what's production-ready today when it comes to modern CSS 2026 features, which libraries you can probably drop, and how to think about the ones worth keeping.
Where We Are With Browser Support
Before getting into features, let's be honest about the support picture, because nothing kills a CSS post faster than breathless claims about things that only work in Chrome Canary.
Here's the state of the features I care about most, pulled from Can I Use as of early 2026:
| Feature | Chrome | Firefox | Safari | Notes |
|---|---|---|---|---|
| Container queries | 106+ | 110+ | 16+ | Fully supported everywhere |
:has() | 105+ | 121+ | 15.4+ | Fully supported everywhere |
| CSS nesting | 120+ | 117+ | 17.2+ | Fully supported everywhere |
| View transitions | 111+ | 144+ | 18+ | Now supported everywhere |
| Anchor positioning | 125+ | 147+ | 26+ | Landed in all browsers January 2026 |
color-mix() | 101+ | 88+ | 15.4+ | Supported since mid-2023 |
@layer | 99+ | 97+ | 15.4+ | Supported since early 2022 |
Container queries, :has(), CSS nesting, and @layer have been in every major browser for over two years. If you have a project that still supports IE11 or very old Safari, that's a separate conversation. For anything modern, these are fair game.
Anchor positioning is the newest of the bunch. Firefox 147 shipped support in early 2026, completing the cross-browser picture. It went from "Chrome-only experiment" to "baseline" in about eighteen months.
Container Queries: The Feature That Actually Changes How You Build Things
Media queries answer the question: how wide is the viewport? Container queries answer a better question: how wide is this component's container?
This sounds like a small difference but it's not. It means you can build a card component that adapts to its context, not to the global viewport. Drop that card into a sidebar or a full-width grid, and it figures out what to do.
Here's a practical example. A product card that switches from vertical to horizontal layout when it has enough room:
.card-wrapper {
container-type: inline-size;
container-name: product-card;
}
.card {
display: flex;
flex-direction: column;
gap: 1rem;
}
.card img {
width: 100%;
aspect-ratio: 16 / 9;
object-fit: cover;
}
@container product-card (min-width: 480px) {
.card {
flex-direction: row;
}
.card img {
width: 200px;
aspect-ratio: 1 / 1;
}
}
That container-type: inline-size is the thing people miss. You have to declare a containment context on the parent before the child can query it. It's one extra line and it's easy to forget, but it's there for a real reason: the browser needs to know that element's dimensions are isolated enough to query against.
The old way to do this was a combination of media queries and prop-drilling in React, often with a ResizeObserver hooked up to set a CSS class or a data attribute. The new way is four lines of CSS that work everywhere.
Real-world adoption is still catching up. Project Wallace's 2026 CSS analysis of over 100,000 websites puts @container usage at 9.61%. That's low, but it doubled year over year. The feature is there. Teams are just slowly discovering it.
:has(): The Parent Selector We've Wanted for Twenty Years
For as long as I can remember, people asked for a parent selector in CSS. The answer was always "no, it would kill browser rendering performance." Then Safari shipped :has() in 2022, the rest of the browsers followed, and nothing caught fire.
:has() lets you style an element based on what it contains. Simple version:
/* A form label that contains a required input */
label:has(input[required]) {
font-weight: 600;
}
label:has(input[required])::after {
content: " *";
color: red;
}
Before this, you'd either add a class to the label via JavaScript or restructure your HTML so you could style things in the right order. Now you don't.
The more interesting use cases involve state. Is a card selected? Does a section have any visible children? Is a nav item currently active?
/* Dim all sibling cards when any card is hovered */
.card-grid:has(.card:hover) .card:not(:hover) {
opacity: 0.6;
}
/* Hide empty sections so they don't leave gaps */
section:not(:has(*)) {
display: none;
}
That second one is something I actually used last month on a project where some optional content sections were conditionally rendered. Before :has(), I was setting display: none via JavaScript. Now I don't touch JavaScript at all.
41.3% of websites already use :has() according to the 2026 CSS Selection report. That's higher adoption than :is(). It's one of the fastest-adopted CSS selectors in recent memory.
Native CSS Nesting: Goodbye, Sass (Mostly)
Sass nesting was the original gateway drug. You could write .card { &:hover { ... } } and feel like a civilized person instead of repeating .card everywhere. Now browsers do it natively.
/* Native CSS nesting */
.nav {
display: flex;
gap: 1rem;
a {
color: var(--color-text);
text-decoration: none;
&:hover {
color: var(--color-accent);
}
&.active {
font-weight: 600;
}
}
}
There's one difference from Sass nesting worth knowing: in native CSS, nested selectors without & are implicitly descendant selectors, same as Sass. But when you need to modify the parent itself (like &:hover), you still need the &. The behavior matches Sass closely enough that converting Sass files is mostly find-and-replace work.
Native nesting shipped in Chrome 120, Firefox 117, and Safari 17.2. All of those hit in 2023, so it's been well over two years. If you're still running a Sass pipeline purely for nesting, that's probably the only real reason left to think about removing it.
I still use Sass on some projects, but only for two things: the @use module system for organizing a large codebase, and a handful of color functions I haven't yet replaced with color-mix(). The nesting use case is fully covered by native CSS now.
The View Transitions API: Page Animations Without JavaScript
If you've ever tried to animate between two pages or two states in a single-page app, you know how much JavaScript it took. You'd reach for Framer Motion, or write a complex AnimationPresence wrapper, or just give up and use a plain fade that took someone two days to build correctly.
The View Transitions API does this in CSS. Mark elements with view-transition-name, call document.startViewTransition() (or in multi-page apps, nothing at all), and the browser handles the crossfade:
/* Shared element: transition the hero image between pages */
.product-image {
view-transition-name: hero-image;
}
/* Customize the animation */
::view-transition-old(hero-image) {
animation: fade-out 200ms ease-out;
}
::view-transition-new(hero-image) {
animation: fade-in 300ms ease-in;
}
@keyframes fade-out {
to { opacity: 0; transform: scale(0.98); }
}
@keyframes fade-in {
from { opacity: 0; transform: scale(0.98); }
}
For multi-page apps (MPA), navigation transitions happen without any JavaScript at all, just @view-transition { navigation: auto; } in your CSS. That's wild. I remember writing a whole React transition system for a project in 2022 that could have been replaced by eight lines of CSS.
Firefox was the last holdout here, shipping it in version 144 in early 2026. All major browsers now support the single-document View Transitions API. Cross-document transitions have slightly patchier support but are moving fast.
Anchor Positioning: The End of JavaScript-Powered Tooltips
Most tooltip and popover libraries in JavaScript exist for one reason: positioning a floating element relative to a trigger. "Show this tooltip above this button, but flip to below if there's not enough room." CSS couldn't do that. So we got Popper.js, then Floating UI, and every UI library shipped its own version.
Anchor positioning changes that:
.button {
anchor-name: --trigger-btn;
}
.tooltip {
position: absolute;
position-anchor: --trigger-btn;
bottom: calc(anchor(top) + 8px);
left: anchor(center);
translate: -50% 0;
position-try-fallbacks: flip-block;
}
The position-try-fallbacks: flip-block line is the clever bit. It tells the browser to try a fallback position (flipped vertically) if the primary position would overflow the viewport. That's the whole reason Floating UI existed.
As of Firefox 147 shipping in early 2026, this works across all major browsers. For new projects, there's a real argument for skipping the JS positioning library entirely.
Old Way vs. New Way
Here are three patterns I've replaced with modern CSS in the last year.
Responsive components with ResizeObserver:
Old way: Hook up a ResizeObserver on a component, set a data-size="small" attribute when the width drops below a threshold, write CSS that styles [data-size="small"] .card-title.
New way: container-type: inline-size on the wrapper, @container (max-width: 400px) in CSS. No JavaScript.
Dark mode with class toggling:
Old way: A JavaScript toggle that adds .dark to the <html> element, then .dark .button { background: #333; } for every component.
New way: CSS custom properties with prefers-color-scheme and the light-dark() function:
:root {
color-scheme: light dark;
--bg: light-dark(white, #1a1a1a);
--text: light-dark(#111, #f0f0f0);
}
.button {
background: var(--bg);
color: var(--text);
}
One place to define both themes. No JavaScript class management. No separate dark-mode stylesheets.
Dynamic color palettes from a base color:
Old way: Use a Sass color function to generate tints and shades at build time, or hard-code ten palette values by hand.
New way: color-mix():
:root {
--brand: #3b7ef8;
--brand-light: color-mix(in oklch, var(--brand) 60%, white);
--brand-dark: color-mix(in oklch, var(--brand) 70%, black);
--brand-subtle: color-mix(in oklch, var(--brand) 15%, white);
}
This runs in the browser, which means it responds to custom property overrides. Change --brand and the whole palette updates. The Sass version was static.
What You Can Actually Drop
I'm not going to pretend everything is replaceable. But here's an honest take on what's become optional:
Sass (for most projects): If you're using it purely for nesting, variables, and color functions, native CSS now covers all three with custom properties, native nesting, and color-mix(). The @use module system still has no native equivalent, so large Sass codebases with lots of @forward and @use have a reason to stay. Small and medium projects? I'd seriously evaluate whether you need it.
CSS-in-JS for scoping: The main reason people reached for styled-components was scoping. CSS Modules do this without runtime cost. And with @layer, you get cascade control that makes specificity fights much less common. The performance argument against runtime CSS-in-JS got a lot louder once React Server Components arrived. You can't ship a runtime style engine in a component that runs only on the server.
Some of Floating UI: For new projects where you control the popover and tooltip behavior, CSS anchor positioning plus the native <popover> attribute cover a lot of ground. Floating UI still makes sense for complex positioning logic with lots of customization, but "I just need a tooltip" doesn't require a library anymore.
My Take on Tailwind in 2026
Tailwind is still genuinely useful. I'll say that clearly because this section can easily turn into a pile-on.
That said, the reasons to choose Tailwind have changed. The utility-class approach isn't uniquely powerful anymore now that you can compose styles reasonably well with custom properties and @layer. The design-token value is real but you can replicate it with a small CSS variables file. The dx argument is still there if your team already knows it.
Where Tailwind still wins for me: large teams where design consistency matters and you want constraints baked into the class names. When everyone uses text-blue-500, you don't get rogue colors creeping in. With plain CSS, people start reaching for whatever hex code is nearby. That problem is real.
Where I'd skip it: small projects, projects with complex animations, anything where you're fighting the utility model more than benefiting from it.
Tailwind v4 made an interesting choice by going CSS-first with an @theme directive instead of tailwind.config.js. It's a framework that's adapting to a world where CSS can express a lot more natively. That's smart.
What I'm Actually Watching
Anchor positioning is the one I'm most excited to use more widely. I still have @floating-ui/react in three projects that I'd love to remove.
Container queries are slowly changing how I think about component design. I'm building components that know nothing about the viewport anymore, only their container. It's a better mental model.
And @layer is underused. It solves the specificity problem that made CSS feel unmanageable on large codebases. Only 2.7% of websites use it, mostly Tailwind-generated, which tells me a lot of teams haven't discovered it yet.
If you haven't touched any of these features, I'd start with :has() and native nesting. They're the lowest friction entry points. You'll be writing better CSS within an hour.
References
- Can I Use: CSS Container Queries
- Can I Use: :has() Selector
- Can I Use: CSS Nesting
- Can I Use: View Transitions API
- Can I Use: CSS Anchor Positioning
- Can I Use: CSS Cascade Layers (@layer)
- MDN: color-mix()
- Project Wallace: The CSS Selection 2026
- LogRocket: Container queries in 2026
- LogRocket: A dev's guide to Tailwind CSS in 2026
- MDN: CSS Nesting
- Reddit: CSS Anchor Positioning lands in all major browsers