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.
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.
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.
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.
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.
/* 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.
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);
}
}
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.
Let me show you three common UI patterns and compare the JavaScript approach (current) with the CSS approach (modern).
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.
/* 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.
document.documentElement
.style.setProperty(
'--bg', isDark
? '#1a1a2e' : '#ffffff'
);
// + 10 more properties...
+ theme toggle logic + localStorage + flash prevention. ~40 lines.
@container style(--theme: dark) {
:root {
--bg: #1a1a2e;
--text: #e4e4e7;
--border: #334155;
}
}
Purely declarative. The --theme custom property toggles everything.
const cols = window.innerWidth > 1024
? 4 : window.innerWidth > 640
? 2 : 1;
grid.style.gridTemplateColumns =
`repeat(${cols}, 1fr)`;
Resize listeners + recalculations. Debounced to avoid jank.
.grid {
grid-template-columns:
attr(data-cols number, 3) 1fr;
}
@media (width < 640px) {
.grid { --cols: 1; }
}
Pure CSS. The browser handles responsiveness natively.
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.
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 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.
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.
You don't need to wait for full browser support to start adopting these patterns. Three steps to begin:
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.
@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.
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.
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.
Tell me about your project — I'll recommend the best architecture and provide a preliminary estimate. Free of charge.