CSS in 2026: What Actually Changed and What Still Matters

By 2026, CSS handles most layout and state work natively. Container queries, :has(), nesting, and cascade layers are baseline across Chrome, Safari, and Firefox. I now skip preprocessors and JS for things like parent styling and responsive components. What still matters is knowing which features ship everywhere and writing plain, maintainable CSS.
Part of the complete web design guide.
On this page
- The stuff that finally works everywhere
- Container queries
- :has()
- Native nesting
- Subgrid
- :is() and :where()
- Modern color
- text-wrap: balance and pretty
- What I’ve stopped doing
- Cascade layers, if you maintain anything large
- A realistic 2026 stack
- FAQ
- Do I still need a CSS preprocessor in 2026?
- Is :has() safe to use in production now?
- What should I learn first if I’m behind?
Most CSS advice on the web is either five years stale or written by someone who has never shipped anything. This is a plain rundown of what has actually changed in CSS by 2026, what I reach for on real projects, and what I’ve quietly stopped using. If you maintain a site that was built before 2023, some of this will save you real time.
The stuff that finally works everywhere
Browser support is the part people get wrong. Features that used to need a polyfill, a preprocessor, or a build step are now baseline across Chrome, Safari, and Firefox. Here’s what I use without a second thought in 2026.
Container queries
Media queries ask about the viewport. Container queries ask about the parent element. That sounds small until you build a card that lives in a wide sidebar on one page and a narrow column on another. Before container queries you wrote two sets of styles and hoped. Now the component styles itself.
.card-wrap {
container-type: inline-size;
container-name: card;
}
@container card (min-width: 380px) {
.card {
display: grid;
grid-template-columns: 120px 1fr;
gap: 1rem;
}
}
I’ve moved most of my card, nav, and form layouts off viewport media queries and onto containers. It makes components genuinely portable, which matters when the same markup shows up in three different page templates.
:has()
The parent selector landed and it’s not a gimmick. I use it constantly for state that used to require JavaScript.
/* Label the field only when it has an invalid value */
.field:has(input:user-invalid) label {
color: #b3261e;
}
/* Add space only when a figure actually has a caption */
figure:has(figcaption) {
margin-block-end: 2rem;
}
/* Style the form when any checkbox is checked */
form:has(input[type="checkbox"]:checked) .submit-bar {
position: sticky;
bottom: 0;
}
That last one replaced about 30 lines of event listeners on a project last year. Fewer moving parts, nothing to debounce, no hydration mismatch.
Native nesting
Nesting is in every current browser, so you can drop Sass for a lot of projects. The one gotcha: a nested selector that starts with an element name is parsed as a type selector, not a descendant. Use & when you mean the parent.
.nav {
display: flex;
gap: 1.5rem;
& a {
color: inherit;
text-decoration: none;
}
/* Without the &, this targets a literal inside .nav */
&:hover a {
text-decoration: underline;
}
}
Subgrid
Subgrid lets a child grid adopt the track lines of its parent. It fixes the “my card headings don’t line up” problem that people used to solve with fixed heights and JavaScript measurement.
.grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1.5rem;
}
.card {
display: grid;
grid-row: span 3;
grid-template-rows: subgrid;
}
Now every card’s title, body, and footer sit on the same horizontal lines no matter how long the text is.
:is() and :where()
:is() takes the specificity of its most specific argument. :where() has zero specificity. That difference is the whole point: use :where() for base styles you want easy to override.
/* Zero specificity, so any single class beats it */
:where(h1, h2, h3) {
line-height: 1.15;
text-wrap: balance;
}
/* Grouped hover states without repeating the selector */
:is(.btn, .link, .chip):hover {
opacity: 0.85;
}
Modern color
OKLCH is my default now. It’s perceptually uniform, which means changing lightness by a fixed amount looks like the same change across every hue. With HSL, a 10% lightness bump on yellow looks nothing like a 10% bump on blue.
:root {
--brand: oklch(62% 0.19 255);
--brand-dark: oklch(48% 0.19 255);
--brand-light: oklch(88% 0.06 255);
}
/* Mix without a preprocessor */
.overlay {
background: color-mix(in oklch, var(--brand) 70%, white);
}
color-mix() and relative color syntax mean I write far fewer hardcoded hex values than I used to. Theme swaps are mostly a matter of redefining a few custom properties.
text-wrap: balance and pretty
Two lines of CSS that replace a headline-testing habit and reduce widows in body copy.
h1, h2, h3 {
text-wrap: balance;
}
p {
text-wrap: pretty;
}
What I’ve stopped doing
- Reaching for Sass by default. Nesting, variables, and color math are native now. I still use a build step for bundling and minification, but the preprocessor layer is often gone.
- Writing float-based layouts. Flexbox and grid have been safe for years. There’s no reason to keep float hacks around.
- Fighting specificity with !important. A layer or a
:where()usually solves it cleanly. - Guessing at responsive breakpoints. I resize the container and watch where it breaks, then set the query there. The old device-width breakpoints are a rough starting point at best.
Cascade layers, if you maintain anything large
If you’ve ever had a third-party stylesheet win a fight it shouldn’t have, layers are the fix. You declare the order once, and later layers always beat earlier ones regardless of specificity.
@layer reset, base, components, utilities;
@layer base {
a { color: navy; }
}
@layer utilities {
.text-brand { color: oklch(62% 0.19 255); }
}
The utility class wins even though both selectors are one class deep, because utilities comes later in the declared order. That’s the behavior you want and it doesn’t require escalating specificity.
A realistic 2026 stack
For a typical marketing site or small app, this is roughly what I ship:
- Native CSS with nesting and custom properties, bundled and minified
- Grid and flexbox for layout, container queries for components
- OKLCH color tokens with
color-mix()for derived shades - Cascade layers to keep base, component, and utility styles from colliding
- A small amount of JavaScript for behavior, not for layout
That’s a lighter stack than I was running in 2020, and it’s faster to debug because there’s less between what I write and what the browser does. If you’re on an older codebase and the styles are a mess of overrides, this is usually where I start: strip the preprocessor, add layers, and move component layout to container queries. I do that kind of cleanup work as a Long Island web design project fairly often, and it’s usually a week of work that pays off for years.
FAQ
Do I still need a CSS preprocessor in 2026?
Usually not. Nesting, variables, and color functions are native. Keep the preprocessor only if you depend on its mixins, loops, or an existing codebase that would be expensive to migrate.
Is :has() safe to use in production now?
Yes. It’s supported in all current major browsers. It’s fine for progressive enhancement too, since a browser without it just skips the rule and you get the unenhanced state.
What should I learn first if I’m behind?
Grid, then container queries, then :has(). Those three cover most of what modern layouts and interactive states need, and they replace the largest amount of old workaround code.


