I deleted 2,400 lines of CSS from a production project last month. Not because I was refactoring or cleaning up dead code. Because CSS itself made those lines unnecessary.

Container queries replaced 600 lines of media query overrides for reusable components. Native CSS nesting eliminated 400 lines of Sass nesting that we were compiling away. The :has() selector killed 300 lines of JavaScript that toggled parent classes. Scroll-driven animations replaced a 45KB JavaScript scroll library and its 200 lines of configuration. And color-mix() plus oklch() condensed our 900-line color system into 80 lines of CSS custom properties.

If you have not been paying close attention to CSS since 2024, you are going to be surprised. The language has changed more in the last two years than in the previous decade. Features that we begged for since 2015 -- parent selectors, container queries, native nesting -- are now shipping in every major browser. And they work. Not "works in Chrome Canary behind a flag" works. Actual, production-ready, cross-browser works.

What Is Production-Ready Right Now

Let me be specific about "production-ready." I mean: works in Chrome, Firefox, Safari, and Edge. Has been stable for at least 6 months. Can be used without polyfills for users on current browsers. The baseline here is browsers released since January 2025, which covers 94%+ of global users as of early 2026.

1. Container Queries

This is the single biggest change to CSS in a decade. Container queries let a component respond to the size of its container, not the size of the viewport. This sounds like a small distinction. It is not.

The old way (media queries):

/* This card component changes layout based on the VIEWPORT width */
.card {
  display: grid;
  grid-template-columns: 1fr;
}

@media (min-width: 768px) {
  .card {
    grid-template-columns: 200px 1fr;
  }
}

@media (min-width: 1200px) {
  .card {
    grid-template-columns: 300px 1fr;
  }
}

The problem: this card component assumes it is always full-width. Put it in a sidebar, and the media query breakpoints are wrong. The card switches to horizontal layout at 768px viewport width, but the sidebar is only 300px wide. The card looks broken.

The new way (container queries):

/* The card's parent is a container */
.card-wrapper {
  container-type: inline-size;
  container-name: card-container;
}

/* The card responds to ITS CONTAINER'S width, not the viewport */
.card {
  display: grid;
  grid-template-columns: 1fr;
}

@container card-container (min-width: 500px) {
  .card {
    grid-template-columns: 200px 1fr;
  }
}

@container card-container (min-width: 800px) {
  .card {
    grid-template-columns: 300px 1fr;
  }
}

Now this card works correctly whether it is in a full-width main content area, a 400px sidebar, or a 600px modal. The breakpoints are relative to the card's container, not the screen.

Real-world impact: On a client project with a design system of 47 components, container queries eliminated the need for variant props like <Card layout="horizontal" /> and <Card layout="vertical" />. The card just figures it out based on available space. We deleted 23 component variants and the JavaScript logic that chose between them.

Container Query Units

Container queries also give you new CSS units:

.card-title {
  /* Font size is 5% of the container's inline (width) size */
  font-size: clamp(1rem, 5cqi, 2rem);

  /* Padding is 3% of the container width */
  padding: 3cqi;
}
Unit Meaning
cqw 1% of container width
cqh 1% of container height
cqi 1% of container inline size
cqb 1% of container block size
cqmin Smaller of cqi and cqb
cqmax Larger of cqi and cqb

These are genuinely useful for fluid typography and spacing within components.

Browser Support

Browser Support Since
Chrome 105 (Sep 2022)
Firefox 110 (Feb 2023)
Safari 16 (Sep 2022)
Edge 105 (Sep 2022)

Verdict: Use it today. No polyfill needed for 95%+ of users.

2. CSS Nesting

Native CSS nesting means you can nest selectors inside other selectors, just like Sass/SCSS:

/* Native CSS nesting - no preprocessor needed */
.nav {
  background: var(--color-surface);
  padding: 1rem;

  & ul {
    display: flex;
    gap: 1rem;
    list-style: none;
  }

  & a {
    color: var(--color-text);
    text-decoration: none;

    &:hover {
      color: var(--color-primary);
    }

    &.active {
      font-weight: 700;
      border-bottom: 2px solid var(--color-primary);
    }
  }

  @media (max-width: 768px) {
    & ul {
      flex-direction: column;
    }
  }
}

Since late 2024, the & prefix is no longer required for element selectors in Chrome, Firefox, and Safari. You can write:

.nav {
  background: var(--color-surface);

  /* No & needed for element selectors (2024+ browsers) */
  ul {
    display: flex;
    gap: 1rem;
  }

  a {
    color: var(--color-text);

    &:hover {
      color: var(--color-primary);
    }
  }
}

What this means for Sass: If you are only using Sass for nesting and variables, you can drop it entirely. CSS custom properties handle variables. Native nesting handles nesting. The two features that drove 90% of Sass adoption are now native.

We still use Sass on some projects for mixins, functions, and @extend. But for new projects at CODERCOPS, we default to vanilla CSS unless we have a specific reason to add a preprocessor.

3. The :has() Selector (Parent Selector)

CSS developers have been asking for a parent selector since approximately forever. :has() is the answer, and it is more powerful than what most people imagined.

:has() selects an element based on its descendants. But it also works for sibling relationships, state checking, and complex conditional styling.

Basic parent selection:

/* Style a card differently when it contains an image */
.card:has(img) {
  grid-template-rows: 200px 1fr;
}

/* Style a card differently when it has no image */
.card:not(:has(img)) {
  padding: 2rem;
}

Form state styling without JavaScript:

/* Style the label when its input is focused */
.form-group:has(input:focus) label {
  color: var(--color-primary);
  transform: translateY(-24px) scale(0.85);
}

/* Style the form group when its input is invalid */
.form-group:has(input:invalid:not(:placeholder-shown)) {
  & label {
    color: var(--color-error);
  }

  & .error-message {
    display: block;
  }
}

/* Disable submit button when any required field is empty */
form:has(input:required:placeholder-shown) button[type="submit"] {
  opacity: 0.5;
  pointer-events: none;
}

That last example is wild. It disables the submit button purely in CSS when any required input is empty. No JavaScript event listeners. No framework state management. Just CSS.

Conditional layout:

/* Switch to grid layout when the list has more than 3 items */
.tag-list:has(:nth-child(4)) {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
}

/* Single-column layout when 3 or fewer items */
.tag-list:not(:has(:nth-child(4))) {
  display: flex;
  gap: 0.5rem;
}

What we replaced: On one project, we had 300 lines of JavaScript that added/removed classes on parent elements based on child state (form validation, dropdown open/closed, content presence). :has() replaced all of it with pure CSS.

4. Subgrid

Subgrid solves the "nested grid alignment" problem that has frustrated CSS developers since Grid launched in 2017.

The problem: you have a grid of cards. Each card has a title, description, and button. You want all the titles to align horizontally, all the descriptions to align, and all the buttons to align at the bottom. Without subgrid, this is nearly impossible because each card creates its own grid context.

/* Parent grid */
.card-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 2rem;
}

/* Each card inherits the parent's row tracks */
.card {
  display: grid;
  grid-row: span 3;
  grid-template-rows: subgrid;
  gap: 0;
}

.card-title {
  align-self: start;
  padding: 1rem;
}

.card-description {
  align-self: start;
  padding: 0 1rem;
}

.card-action {
  align-self: end;
  padding: 1rem;
}

With subgrid, the card's internal rows inherit the parent grid's row sizing. This means if one card has a long title, the title row expands for all cards. Everything stays aligned.

Before subgrid, the common workaround was either flexbox with flex-grow: 1 on the middle section (which does not truly align) or JavaScript that measured and equalized heights (brittle and slow).

5. Color Functions: oklch() and color-mix()

CSS color management got a massive upgrade. Two functions change how we build design system color palettes.

oklch() -- Perceptually Uniform Colors:

:root {
  /* oklch(lightness chroma hue) */
  --color-primary: oklch(55% 0.25 260);        /* Vibrant blue */
  --color-primary-light: oklch(75% 0.15 260);  /* Same hue, lighter */
  --color-primary-dark: oklch(35% 0.25 260);   /* Same hue, darker */

  /* Generate an entire palette by adjusting lightness */
  --color-primary-50: oklch(97% 0.03 260);
  --color-primary-100: oklch(93% 0.06 260);
  --color-primary-200: oklch(85% 0.10 260);
  --color-primary-300: oklch(75% 0.15 260);
  --color-primary-400: oklch(65% 0.20 260);
  --color-primary-500: oklch(55% 0.25 260);
  --color-primary-600: oklch(45% 0.22 260);
  --color-primary-700: oklch(35% 0.18 260);
  --color-primary-800: oklch(25% 0.14 260);
  --color-primary-900: oklch(15% 0.10 260);
}

The key advantage of oklch over hsl: equal lightness values actually look equally bright. In HSL, hsl(60, 100%, 50%) (yellow) looks way brighter than hsl(240, 100%, 50%) (blue) even though they have the same 50% lightness value. OKLCH fixes this perceptual problem.

color-mix() -- Programmatic Color Blending:

:root {
  --color-brand: oklch(55% 0.25 260);

  /* Create hover states by mixing with black/white */
  --color-brand-hover: color-mix(in oklch, var(--color-brand), black 15%);
  --color-brand-active: color-mix(in oklch, var(--color-brand), black 25%);
  --color-brand-subtle: color-mix(in oklch, var(--color-brand), white 80%);

  /* Create transparent versions */
  --color-brand-10: color-mix(in oklch, var(--color-brand) 10%, transparent);
  --color-brand-50: color-mix(in oklch, var(--color-brand) 50%, transparent);
}

.button {
  background: var(--color-brand);

  &:hover {
    background: var(--color-brand-hover);
  }

  &:active {
    background: var(--color-brand-active);
  }
}

What we replaced: Our old color system had 900 lines of CSS defining every shade manually. We had variables like --blue-100, --blue-200, through --blue-900, for each of 8 brand colors. With oklch() and color-mix(), we define one base color per brand color and derive everything else. 900 lines became 80.

6. Scroll-Driven Animations

This is the feature that will eliminate the most JavaScript from your projects. Scroll-driven animations let you animate elements based on scroll position -- no JavaScript, no Intersection Observer, no scroll event listeners.

Scroll progress indicator (the classic use case):

/* A reading progress bar that fills as you scroll */
.progress-bar {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 3px;
  background: var(--color-primary);
  transform-origin: left;
  transform: scaleX(0);
  animation: grow-progress linear;
  animation-timeline: scroll();
}

@keyframes grow-progress {
  from { transform: scaleX(0); }
  to { transform: scaleX(1); }
}

That is it. No JavaScript. No scroll event listener. No requestAnimationFrame. No debouncing. Just CSS.

Fade-in on scroll (replacing AOS, ScrollReveal, etc.):

.fade-in-section {
  opacity: 0;
  transform: translateY(30px);
  animation: fade-in-up linear both;
  animation-timeline: view();
  animation-range: entry 0% entry 100%;
}

@keyframes fade-in-up {
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

The animation-timeline: view() declaration ties the animation to the element's visibility in the viewport. animation-range: entry 0% entry 100% means the animation runs from when the element starts entering the viewport to when it is fully visible.

Parallax effects without JavaScript:

.parallax-bg {
  animation: parallax linear;
  animation-timeline: scroll();
}

@keyframes parallax {
  from { transform: translateY(0); }
  to { transform: translateY(-200px); }
}

.parallax-slow {
  animation: parallax linear;
  animation-timeline: scroll();
}

@keyframes parallax-slow {
  from { transform: translateY(0); }
  to { transform: translateY(-80px); }
}

What we replaced: On a recent client project, we removed the AOS (Animate On Scroll) library entirely. That library was 45KB minified, required initialization JavaScript, and added scroll event listeners that could jank on lower-end devices. The CSS-only replacement is 0 bytes of JavaScript and smoother because it uses the browser's compositor thread.

7. View Transitions API

Page transitions without a framework. This works for both single-page apps (SPAs) and multi-page apps (MPAs).

/* Define transition animations */
@view-transition {
  navigation: auto;
}

/* Customize the transition */
::view-transition-old(root) {
  animation: fade-out 0.2s ease-out;
}

::view-transition-new(root) {
  animation: fade-in 0.3s ease-in;
}

@keyframes fade-out {
  from { opacity: 1; }
  to { opacity: 0; }
}

@keyframes fade-in {
  from { opacity: 0; }
  to { opacity: 1; }
}

/* Shared element transitions */
.post-thumbnail {
  view-transition-name: post-image;
}

.post-title {
  view-transition-name: post-title;
}

When you navigate from a blog listing to a blog post, the thumbnail and title can smoothly animate from their listing position to their post position. This used to require a full SPA framework with animation libraries like Framer Motion or GSAP.

Astro has built-in view transition support, and we use it on codercops.com for page navigations. The effect is subtle but noticeably polished.

8. Anchor Positioning

Tooltips, popovers, and dropdown menus without JavaScript positioning libraries. No more Popper.js or Floating UI for basic positioning.

/* The anchor (trigger element) */
.tooltip-trigger {
  anchor-name: --my-tooltip;
}

/* The tooltip positions itself relative to its anchor */
.tooltip {
  position: fixed;
  position-anchor: --my-tooltip;

  /* Position at the top center of the anchor */
  bottom: anchor(top);
  left: anchor(center);
  transform: translateX(-50%);

  /* Fallback positioning if tooltip would go off-screen */
  position-try-fallbacks: --bottom, --left, --right;
}

@position-try --bottom {
  top: anchor(bottom);
  bottom: auto;
}

@position-try --left {
  right: anchor(left);
  left: auto;
  bottom: anchor(center);
  transform: translateY(50%);
}

@position-try --right {
  left: anchor(right);
  right: auto;
  bottom: anchor(center);
  transform: translateY(50%);
}

The position-try-fallbacks feature is the killer detail. The browser automatically repositions the tooltip if it would overflow the viewport. This is exactly what Popper.js/Floating UI do, but natively and without JavaScript.

Browser support note: Anchor positioning is the newest feature on this list. Chrome has full support since version 125 (May 2024). Firefox added support in early 2025. Safari added support in mid-2025. For production use in early 2026, I would recommend a progressive enhancement approach -- the tooltip still works without anchor positioning, it just does not have automatic repositioning.

What You Can Drop

Based on what is now production-ready, here is what you can remove from your projects:

Sass / Less (For Most Projects)

Keep if: You need mixins, @extend, complex functions, or have a massive existing Sass codebase.

Drop if: You are only using Sass for nesting and variables.

CSS custom properties replace Sass variables with runtime advantages (they can change in media queries and with JavaScript). CSS nesting replaces Sass nesting. For new projects, vanilla CSS is enough.

/* You used to need Sass for this. Not anymore. */
:root {
  --spacing-sm: 0.5rem;
  --spacing-md: 1rem;
  --spacing-lg: 2rem;
  --color-text: oklch(20% 0 0);
}

.card {
  padding: var(--spacing-md);

  .title {
    font-size: 1.25rem;
    color: var(--color-text);
  }

  .body {
    margin-top: var(--spacing-sm);
  }

  &:hover {
    box-shadow: 0 4px 12px color-mix(in oklch, black 15%, transparent);
  }
}

JavaScript Scroll Libraries

Drop: AOS, ScrollReveal, Locomotive Scroll (for basic scroll animations), custom Intersection Observer implementations for fade-in effects.

Keep: GSAP (if you need complex timeline animations), Lenis (if you need custom scroll behavior, not just scroll-triggered animations).

Autoprefixer (For Most Properties)

The days of -webkit- and -moz- prefixes for common properties are over. Grid, flexbox, custom properties, transforms, transitions -- none of these need prefixes for browsers released after 2022.

Still needs prefixes: backdrop-filter in some older Safari versions, -webkit-line-clamp. But autoprefixer's value has diminished significantly.

JavaScript Tooltip/Popover Positioning Libraries

Drop (with progressive enhancement): Popper.js, Floating UI, Tippy.js (for basic tooltip positioning).

Keep: If you need IE11 support (but why?) or if your tooltips need complex behavior beyond positioning.

Many Breakpoint Utility Classes

With container queries, the pattern of .col-md-6 and .col-lg-4 becomes less relevant. Components can be responsive to their container without knowing about the viewport.

/* Old pattern: viewport-based grid columns */
.grid-item { width: 100%; }
@media (min-width: 768px) { .grid-item { width: 50%; } }
@media (min-width: 1200px) { .grid-item { width: 33.33%; } }

/* New pattern: container-based responsiveness */
.grid-container { container-type: inline-size; }

.grid-item { width: 100%; }
@container (min-width: 500px) { .grid-item { width: 50%; } }
@container (min-width: 800px) { .grid-item { width: 33.33%; } }

Browser Support Reality Check

Here is the honest support table for every feature discussed:

Feature Chrome Firefox Safari Edge Global Support
Container queries 105+ 110+ 16+ 105+ 95%+
CSS nesting 120+ 117+ 17.2+ 120+ 92%+
:has() 105+ 121+ 15.4+ 105+ 93%+
Subgrid 117+ 71+ 16+ 117+ 93%+
oklch() 111+ 113+ 15.4+ 111+ 93%+
color-mix() 111+ 113+ 16.2+ 111+ 92%+
Scroll-driven animations 115+ 110+ (partial) 18+ 115+ 85%+
View Transitions 111+ 130+ 18+ 111+ 83%+
Anchor positioning 125+ 131+ 18+ 125+ 78%+

For the first six features (container queries through color-mix), global support is above 92%. You can use these confidently today.

For scroll-driven animations and view transitions, support is around 83-85%. Use them as progressive enhancement -- the feature works in supported browsers and degrades gracefully in older ones.

For anchor positioning, support is around 78% and growing. Use with a fallback positioning strategy.

Our CSS Stack at CODERCOPS

Here is what we use on new projects as of early 2026:

Base: Vanilla CSS with custom properties. No preprocessor by default.

Methodology: Loosely based on BEM naming for components, but we let CSS nesting and :has() do work that used to require BEM modifier classes.

Scoping: Astro's built-in scoped styles for component CSS. No CSS-in-JS runtime.

Design tokens: CSS custom properties organized in a :root declaration block, with oklch() for colors and a consistent spacing scale.

Reset: A minimal custom reset (about 40 lines) based on Andy Bell's "A More Modern CSS Reset."

Tooling: Just the browser DevTools. CSS has gotten good enough that we rarely need external tools.

/* Our typical project setup */
:root {
  /* Colors (oklch for perceptual uniformity) */
  --color-brand: oklch(55% 0.20 250);
  --color-brand-hover: color-mix(in oklch, var(--color-brand), black 15%);
  --color-brand-subtle: color-mix(in oklch, var(--color-brand), white 85%);
  --color-text: oklch(20% 0.02 250);
  --color-text-muted: oklch(45% 0.02 250);
  --color-surface: oklch(99% 0.005 250);
  --color-border: oklch(88% 0.01 250);

  /* Spacing (consistent scale) */
  --space-xs: 0.25rem;
  --space-sm: 0.5rem;
  --space-md: 1rem;
  --space-lg: 1.5rem;
  --space-xl: 2rem;
  --space-2xl: 3rem;
  --space-3xl: 5rem;

  /* Typography */
  --font-body: 'Inter', system-ui, sans-serif;
  --font-heading: 'Inter', system-ui, sans-serif;
  --font-mono: 'JetBrains Mono', monospace;

  /* Transitions */
  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
  --duration-fast: 150ms;
  --duration-normal: 250ms;
}

/* Dark mode via color-scheme and custom properties */
@media (prefers-color-scheme: dark) {
  :root {
    --color-text: oklch(90% 0.02 250);
    --color-text-muted: oklch(65% 0.02 250);
    --color-surface: oklch(15% 0.02 250);
    --color-border: oklch(28% 0.02 250);
    --color-brand-subtle: color-mix(in oklch, var(--color-brand), black 70%);
  }
}

Performance Impact

Adopting modern CSS features has a measurable impact on performance:

Reduced JavaScript bundle size. By replacing scroll animation libraries, tooltip positioning libraries, and parent-class-toggling scripts with CSS, we typically remove 50-100KB of JavaScript from a project. On a recent e-commerce client, this reduced Time to Interactive by 0.8 seconds on mobile.

Fewer DOM operations. JavaScript-based scroll animations and class toggling cause layout thrashing. CSS animations run on the compositor thread and do not block the main thread. Our scroll animations went from occasionally janky (JavaScript) to butter-smooth (CSS compositor).

Smaller stylesheets. CSS nesting and :has() reduce selector duplication. Container queries reduce the total number of rules needed. On a design system with 47 components, the CSS output dropped from 84KB to 52KB after adopting modern features -- a 38% reduction.

Metric Before (legacy CSS) After (modern CSS) Improvement
Total CSS size 84 KB 52 KB -38%
Total JS (scroll/tooltip libs) 95 KB 0 KB -100%
Render-blocking CSS 84 KB 52 KB -38%
Time to Interactive (3G) 3.2s 2.1s -34%
CLS (Cumulative Layout Shift) 0.08 0.02 -75%

How to Migrate Incrementally

You do not need to rewrite your entire stylesheet. Here is our recommended adoption order:

Week 1: CSS custom properties and color-mix(). Replace Sass/Less variables with custom properties. Adopt oklch() for your color palette. This is the lowest-risk change and provides immediate benefits.

Week 2: CSS nesting. Refactor one component file at a time to use native nesting. If you are using Sass, you can do this gradually -- Sass compiles native nesting syntax just fine, so the refactored code works with or without Sass.

Week 3: :has() for parent styling. Identify JavaScript that toggles parent classes based on child state. Replace with :has() selectors. Start with form validation styling.

Week 4: Container queries for one component. Pick your most reusable component (cards, media objects) and convert its media queries to container queries. See the difference it makes.

Month 2: Scroll-driven animations. Replace your scroll animation library with CSS scroll-driven animations. Use @supports (animation-timeline: scroll()) for progressive enhancement:

/* Fallback for browsers without scroll-driven animations */
.fade-in {
  opacity: 1; /* Visible by default */
}

/* Enhanced experience in supporting browsers */
@supports (animation-timeline: scroll()) {
  .fade-in {
    opacity: 0;
    animation: fade-in linear both;
    animation-timeline: view();
    animation-range: entry 0% entry 100%;
  }
}

Month 3: View transitions and subgrid. These are nice-to-haves that add polish. Adopt them when you have time for progressive enhancement testing.

What Is Coming Next

A few CSS features are in various stages of browser implementation that are worth watching:

CSS Functions and Mixins (CSS Working Group draft): Custom @function and @mixin at-rules. This would bring the last two Sass features to native CSS. Currently in early draft stage. Do not expect browser support before late 2026 at the earliest.

if() conditional function: Write conditional logic directly in CSS property values. width: if(style(--variant: large), 200px, 100px);. In draft stage.

Masonry layout: A native CSS grid masonry mode. Chrome has been experimenting with grid-template-rows: masonry since 2024. Safari has an alternative proposal. The CSS Working Group is still debating which approach to standardize.

None of these are production-ready. I mention them so you know what is on the horizon, not so you start using them.

The Bottom Line

CSS in 2026 is not the CSS you learned in 2020. If you are still writing media query breakpoints for component layouts, toggling parent classes with JavaScript, importing scroll animation libraries, or maintaining a Sass build pipeline just for nesting and variables -- you are writing more code than you need to.

The features covered in this post are not experimental. They are in stable browser releases with 85-95% global support. They work. They are fast. And they let you delete code, which is the best kind of performance optimization.

Start with container queries and :has(). Those two features alone will change how you think about CSS architecture.


Need help modernizing your frontend stack? At CODERCOPS, we build high-performance websites using modern CSS, Astro, and the latest web platform features. Browse our engineering blog for more deep dives, or get in touch to discuss your project.

Comments