CSS Is Eating JavaScript: Modern CSS Patterns That Replace JS
Feature Overview · Updated June 2026

CSS Is Eating JavaScript:
Modern CSS Patterns That Replace JS

When Kevin Powell took the stage at CSS Day 2026 in Amsterdam today, he showed something remarkable: CSS now has functions, queries, and expressions that were exclusively JavaScript territory a year ago. The line between CSS and JS is blurring — and that's a good thing.

Oleg Maximov June 12, 2026 14 min read CSS Day 2026, Amsterdam

The Shift: From Imperative to Declarative

For the past decade, web development followed a simple rule: HTML for structure, CSS for presentation, JavaScript for behavior. But as web applications grew more complex, JavaScript absorbed more and more of what used to be pure CSS territory — responsive design, theme switching, scroll effects, even layout decisions.

That trend is now reversing. CSS is reclaiming its territory, and doing so with tools that are more performant, more declarative, and less error-prone than the JavaScript workarounds they replace.

Kevin Powell's talk at CSS Day 2026 demonstrated three capabilities that represent this shift: CSS if() for inline conditional values, advanced attr() for reading HTML data in CSS, and style queries (@container style()) for component state-based styling. Together, they eliminate entire categories of JavaScript boilerplate.

Let me walk through each one with real code examples and show you what they replace.

CSS if() — Inline Conditional Values

The CSS if() function, part of the CSS Values Module Level 5 specification, brings inline conditional logic to property values. The syntax mirrors JavaScript's ternary operator:

/* Basic conditional padding */
.element {
  padding: if(style(--compact), 0.5rem, 1.5rem);
}

/* Multiple conditions */
.element {
  font-size: if(
        style(--size: small), 0.875rem,
        style(--size: large), 1.25rem,
        1rem
  );
}

The if() function evaluates a condition — typically a style query against a custom property — and returns one of two values. This is purely CSS. No JavaScript. No class toggling. No style.setProperty() calls.

Think about what this replaces. Every time you've written something like:

// JavaScript version — three lines for a one-line concern
if (isCompact) {
  element.style.padding = '0.5rem';
} else {
  element.style.padding = '1.5rem';
}

That's not just code — it's cognitive overhead. The style logic lives in a JavaScript file, removed from where the style is declared. if() keeps the conditional right where it belongs: in the CSS rule.

Nesting and Composition

CSS if() composes naturally with calc(), clamp(), and other CSS functions:

.card {
  /* Compose if() with other functions */
  width: calc(100% - if(style(--sidebar: true), 320px, 0px));

  /* Use in shorthand properties */
  margin: if(style(--compact), 0.5rem 1rem, 1.5rem 2rem);
}

/* Works with any property that accepts values */
.button {
  --bg-light: #f0f4ff;
  --bg-dark: #1e293b;

  background: if(style(--theme: dark), var(--bg-dark), var(--bg-light));
  color: if(style(--theme: dark), #e2e8f0, #1e293b);
  border-color: if(style(--theme: dark), #334155, #cbd5e1);
}

The key insight: if() doesn't replace JavaScript conditionals for business logic. It replaces them for style decisions — which is where they always should have been.

Advanced attr() — Reading HTML Values in CSS

The attr() function has existed in CSS for decades, but with a crippling limitation: it only worked in the content property on pseudo-elements, and always returned a string. That changes in 2026.

The new typed attr() accepts a type parameter and can be used in any CSS property:

/* HTML */
<div data-width="300" data-color="oklch(0.5 0.2 240)">
  This div reads its styles from HTML attributes
</div>

/* CSS — attr() returns typed values */
div {
  width: attr(data-width number, 100%) px;
  background: oklch(from attr(data-color color) l c h / 0.2);
  padding: attr(data-padding length, 1rem);
}

/* Column layout from HTML data */
.grid {
  grid-template-columns: attr(data-cols number, 3) 1fr;
}

The type parameter opens up CSS properties that were previously unreachable from HTML data attributes. Supported types include:

This eliminates the pattern of reading data attributes in JavaScript, parsing them, and applying them as inline styles. The CSS engine handles it directly, which means faster paint cycles and zero JavaScript overhead.

Real-World Example: Dynamic Progress Bar

/* HTML — value lives in the data attribute */
<progress-bar data-value="73" data-max="100"></progress-bar>

/* CSS — no JS needed for the visual */
progress-bar {
  display: block;
  height: 8px;
  background: #e2e8f0;
  border-radius: 4px;
  overflow: hidden;
}
progress-bar::before {
  content: '';
  display: block;
  height: 100%;
  width: calc(attr(data-value number) / attr(data-max number) * 100%);
  background: linear-gradient(90deg, #3b82f6, #6366f1);
  border-radius: 4px;
  transition: width 0.3s ease;
}

Before typed attr(), this required JavaScript to read data-value, calculate the percentage, and set element.style.width. Now the browser handles it natively.

Style Queries — Component State Without JavaScript

CSS container queries (@container) were a breakthrough when they shipped — querying an element's size instead of the viewport. But size isn't the only dimension that matters. Enter style queries: @container style().

Style queries check the computed values of CSS properties on a container, not its dimensions. This enables component-level state logic entirely in CSS:

/* Define a container */
.card-container {
  container-type: inline-size;
}

/* Style query — check theme */
@container style(--theme: dark) {
  .card {
    background: #1a1a2e;
    color: #e4e4e7;
    border-color: #334155;
  }
}

/* Style query — check state */
@container style(--state: error) {
  .card {
    border-color: #ef4444;
    background: #fef2f2;
  }
}

@container style(--state: success) {
  .card {
    border-color: #22c55e;
    background: #f0fdf4;
  }
}

The brilliance of style queries is that they work with any CSS property, not just custom properties. You can query display, position, flex-direction, or any other computed value:

/* Adapt children to the container's layout mode */
@container style(flex-direction: column) {
  .item { margin-bottom: 1rem; }
}
@container style(flex-direction: row) {
  .item { margin-right: 1rem; }
}

/* Style based on computed overflow state */
@container style(overflow: hidden) {
  .truncated-content { position: relative; }
  .truncated-content::after {
    content: '';
    position: absolute;
    bottom: 0;
    left: 0;
    right: 0;
    height: 2rem;
    background: linear-gradient(transparent, white);
  }
}

What Style Queries Replace

Style queries eliminate the most common JavaScript pattern in UI components: class toggling based on state. Every React/Vue/Angular component that does something like this:

// JavaScript class toggling — CSS can now handle this
function Card({ theme, state }) {
  return (
    <div className={`card ${theme} ${state}`}>
      ...
    </div>
  );
}

That's not a bad pattern. But it requires JavaScript to run on every state change. Style queries let the browser's CSS engine handle state transitions instantly, without the JavaScript event loop. The result: smoother animations, less main-thread work, and simpler component code.

Before and After: JS vs CSS Side by Side

Let me show you three common UI patterns and compare the JavaScript approach (current) with the CSS approach (modern).

1. Tab Component

JavaScript Approach

tabs.forEach(tab => {
  tab.addEventListener('click', () => {
    panels.forEach(p => p.classList.remove('active'));
    tab.parentElement.dataset.active = tab.dataset.tab;
  });
});

+ CSS class toggling + event listeners + state management. ~20 lines.

CSS Approach

/* Style query checks data-active */
@container style(--active: tab-2) {
  #tab-2 { display: block; }
}

/* HTML click handler updates attribute */
tab-group::part(tab):focus-visible {
  outline: 2px solid blue;
}

0 lines of JS. HTML handles state via attribute changes.

2. Theme Switching

JavaScript Approach

document.documentElement
  .style.setProperty(
    '--bg', isDark
      ? '#1a1a2e' : '#ffffff'
  );
// + 10 more properties...

+ theme toggle logic + localStorage + flash prevention. ~40 lines.

CSS Approach

@container style(--theme: dark) {
  :root {
    --bg: #1a1a2e;
    --text: #e4e4e7;
    --border: #334155;
  }
}

Purely declarative. The --theme custom property toggles everything.

3. Responsive Grid Columns

JavaScript Approach

const cols = window.innerWidth > 1024
  ? 4 : window.innerWidth > 640
  ? 2 : 1;
grid.style.gridTemplateColumns =
  `repeat(${cols}, 1fr)`;

Resize listeners + recalculations. Debounced to avoid jank.

CSS Approach

.grid {
  grid-template-columns:
    attr(data-cols number, 3) 1fr;
}

@media (width < 640px) {
  .grid { --cols: 1; }
}

Pure CSS. The browser handles responsiveness natively.

The CSS Toolbox — What's Already Here

The features above are the headline acts, but they join a CSS toolbox that's been steadily eating away at JavaScript's territory for the last few years. Let me connect the dots:

:has() — Parent Selector

CSS :has() lets you style an element based on its children or descendants — the "parent selector" developers wanted for decades. Before :has(), you needed JavaScript to add a class to a parent when a child was checked, focused, or present.

/* Style a card when its checkbox child is checked */
.card:has(input[type="checkbox"]:checked) {
  border-color: var(--accent);
  background: rgba(15, 118, 110, 0.05);
}

/* Style a form group that has an invalid input */
.form-group:has(:invalid) .error-message {
  display: block;
}

/* Style the whole row when a specific cell has data */
tr:has(td:empty) { opacity: 0.5; }

I wrote a detailed guide on CSS Container Queries covering size- and style-based queries together. The combination of :has() and style queries eliminates the vast majority of "if-this-then-that" JavaScript in UI components.

Anchor Positioning — Replace JS Popover Libraries

CSS Anchor Positioning (full guide here) lets you position an element relative to another element without JavaScript. Tooltips, popovers, dropdowns, and contextual menus that required libraries like Popper.js or Floating UI can now be pure CSS:

.tooltip {
  position: absolute;
  position-anchor: --trigger;
  top: anchor(--trigger bottom);
  left: anchor(--trigger center);
  translate: -50% 8px;
}

Scroll-Driven Animations

Scroll-driven animations let you link animation progress to scroll position — no IntersectionObserver, no scroll event listeners, no requestAnimationFrame throttling. The browser handles everything on the compositor thread:

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

.reveal {
  animation: fade-in linear forwards;
  animation-timeline: view();
  animation-range: entry 0% entry 100%;
}

This replaces not just JavaScript, but entire scroll-triggered animation libraries with one CSS declaration.

Where CSS Still Needs JavaScript

To be clear: CSS is not replacing JavaScript. CSS is absorbing presentation JavaScript — the imperative code that manages visual state, positions elements, and orchestrates animations. CSS was always the right tool for these jobs; we just needed the CSS to catch up.

JavaScript is still the right tool for:

The goal isn't "zero JavaScript." The goal is right-sized JavaScript — using JS where it adds value and CSS where it does the job better.

Migration Strategy: Start Today

You don't need to wait for full browser support to start adopting these patterns. Three steps to begin:

1. Audit Your Class Toggling

Every time you write element.classList.toggle('active'), ask: "Can a custom property do this instead?" Replace class-based state with --state custom properties and let style queries handle the visual consequences.

2. Use @supports() for Progressive Enhancement

.accordion-panel {
  display: none; /* JS fallback */
}

@supports (container-type: style) {
  .accordion-panel {
    display: revert;
  }

  @container style(--open: true) {
    .accordion-panel {
      display: block;
    }
  }
}

Browsers that don't support style queries get the JavaScript version. Modern browsers get the CSS-native version. No breakage, no polyfills.

3. Replace One JS Library at a Time

Start with the easiest win: remove Popper.js/Floating UI in favor of CSS Anchor Positioning. Then replace scroll-based JS libraries with scroll-driven animations. Then tackle accordions, tabs, and theme switching with style queries and if().

Each replacement removes a JavaScript dependency, reduces bundle size, and eliminates a category of runtime bugs. The cumulative effect is dramatic.

FAQ

Can CSS if() replace JavaScript conditionals in styles?
CSS if() provides inline conditional values, similar to JavaScript's ternary operator, but limited to CSS property values. It works with custom properties and style queries. It cannot replace complex JavaScript logic but eliminates the need for JS-driven class toggling for simple conditional styling.
What can the new CSS attr() function do?
CSS attr() now supports typed values. Instead of returning only strings, it can return numbers, lengths, colors, angles, and more. This means you can read HTML data attributes directly in CSS properties like width, height, padding, color, and grid-template-columns — no JavaScript needed to bridge HTML values to styles.
What are CSS style queries?
CSS style queries (@container style()) allow you to query the computed value of a CSS property or custom property on a container element. For example, @container style(--theme: dark) { .card { background: #1a1a2e; } }. This enables component state-based styling without JavaScript class toggling.
How much JavaScript can CSS realistically replace in 2026?
In a typical UI-heavy application, CSS can replace 40-70% of presentation JavaScript — class toggling, state-based styling, simple conditional rendering, responsive layouts, tab/accordion components, tooltip positioning, and scroll-based effects. Business logic, data fetching, form validation, and complex interactivity still require JavaScript.
Which browsers support CSS if(), style queries, and typed attr()?
Style queries (@container style()) are supported in Chrome 111+, Firefox 110+, and Safari 18+. CSS if() is still in early specification (CSS Values 5) — Chrome Canary has experimental support behind a flag. Typed attr() is in Chrome 128+ with initial support for length and number types. Browser support is growing fast, and you can use progressive enhancement with @supports() for graceful fallbacks.
Is CSS replacing JavaScript a good thing?
Yes — it's a sign of the web platform maturing. Declarative CSS solutions are more performant, more accessible, and less error-prone than imperative JavaScript alternatives. Less JS means smaller bundles, faster rendering, and fewer bugs. But CSS handles presentation logic best; business logic and interactivity still belong in JavaScript. The right tool for each job.
How do I start migrating from JS to CSS patterns?
Start with low-risk patterns: replace JS class toggling with custom properties, swap JS accordions/tabs with style queries, use has() for parent-aware styling instead of JS, adopt anchor positioning for tooltips instead of JS position libraries, and replace scroll-based JS effects with scroll-driven animations. Each change reduces JS bundle size without changing user experience.

Want to Build a Leaner Web Application?

Modern CSS patterns let you build UIs that are faster, smaller, and more maintainable. If you're planning a web project and want architecture that minimizes JavaScript overhead while maximizing performance, I'd love to help.

I'm a full-stack web developer based in Minsk, working worldwide. With 20+ years of experience building in React, Vue, Angular, Node.js, and modern CSS architectures, I can design and build your project using the right tools — from server components to CSS-native interactivity. Let's discuss your project.

Contact

Let's discuss your project

Tell me about your project — I'll recommend the best architecture and provide a preliminary estimate. Free of charge.