Modern CSS Replacing JavaScript
The most significant shift in web development over the past three years has been the transfer of responsibilities from JavaScript to CSS. Features that previously required entire libraries — positioning, scroll effects, page transitions, element visibility detection, intrinsic size animation, and component-responsive layouts — are now achievable with pure CSS or minimal declarative HTML. This isn’t just about reducing bundle size; CSS-based implementations run on the compositor thread, bypass the main thread entirely, and degrade gracefully when unsupported. The result: faster, more resilient user interfaces with dramatically less code.
Container Queries: Component-Responsive Design
Container queries are to components what media queries are to viewports. Instead of asking “how wide is the screen?”, you ask “how wide is my container?” — making components truly self-contained and reusable regardless of where they’re placed in the layout.
/* Define a containment context */
.card-wrapper {
container-type: inline-size;
container-name: card;
}
/* Respond to the container's width, not the viewport */
@container card (min-width: 400px) {
.card {
display: grid;
grid-template-columns: 200px 1fr;
gap: 1rem;
}
}
@container card (max-width: 399px) {
.card { display: block; }
.card img { width: 100%; }
}
Container queries are Baseline across all modern browsers since February 2023 (Chrome 105+, Safari 16+, Firefox 110+). They fundamentally change how we think about responsive design: a card component that adapts to its container rather than the viewport works identically whether placed in a full-width content area, a narrow sidebar, or a modal dialog — no JavaScript ResizeObserver, no duplicate component variants.
Josh W. Comeau’s “Container Queries Unleashed” demonstrates the most powerful pattern: combining container queries with named containers to create multi-layered responsive UIs. Ahmad Shadeed’s extensive interactive examples at ishadeed.com showcase real-world use cases from art-directed feature images to adaptive navigation patterns.
Container query units (cqw, cqh, cqi, cqb, cqmin, cqmax) allow sizing elements relative to their container rather than the viewport, enabling truly self-contained component typography scaling.
Style queries (@container style(--theme: dark)) allow querying the computed value of a custom property on the container — effectively CSS-only conditional styling based on state, without JavaScript.
Resources:
- CSS Container Queries Are Finally Here — Ahmad Shadeed
- Container Queries Unleashed — Josh W. Comeau
- CSS Container Queries — MDN
- What Else Could Container Queries Query? — CSS-Tricks
- Container Queries Lab — Ahmad Shadeed
:has() — The “Parent Selector” and Beyond
:has() is arguably the most powerful selector ever added to CSS. It allows styling an element based on the presence, state, or content of its descendants, siblings, or other relational criteria. It effectively replaces dozens of JavaScript patterns for conditional styling.
/* Style a card differently when it contains an image */
.card:has(img) {
grid-template-rows: 200px 1fr;
}
/* Style the label when its associated input is invalid */
label:has(+ input:invalid) {
color: red;
}
/* Restyle the page when a dialog is open */
body:has(dialog[open]) {
overflow: hidden;
filter: blur(2px);
}
/* Style a form when any required field is empty */
form:has(:required:placeholder-shown) {
.submit-btn { opacity: 0.5; pointer-events: none; }
}
/* Quantity queries: style differently based on child count */
.grid:has(> :nth-child(4)) {
grid-template-columns: repeat(2, 1fr);
}
.grid:has(> :nth-child(7)) {
grid-template-columns: repeat(3, 1fr);
}
:has() is Baseline across all modern browsers (Chrome 105+, Safari 15.4+, Firefox 121+). The State of CSS 2025 survey identifies :has() alongside Grid and CSS Nesting as the three features that have most changed how developers write CSS. Before :has(), every example above required JavaScript event listeners, DOM observation, or framework state management.
The performance note: browsers have optimized :has() extensively, but overly broad selectors like *:has(*) can be expensive. Keep :has() selectors as specific as possible.
Resources:
- :has() — MDN
- Using :has() as a CSS Parent Selector — Jen Simmons, webkit.org
- :has() Interactive Guide — Ahmad Shadeed
- Practical Uses of :has() — Adrian Bece, Smashing Magazine
CSS Anchor Positioning: Tethered Elements Without JavaScript
CSS Anchor Positioning (Chrome 125+, part of Interop 2025, progressing in Safari and Firefox) eliminates the need for JavaScript positioning libraries like Floating UI, Popper.js, and Tippy.js. It lets you natively position any element relative to another element anywhere in the DOM — tooltips, popovers, dropdown menus, footnotes, and annotation overlays.
/* Define an anchor */
.trigger-btn {
anchor-name: --my-anchor;
}
/* Position relative to the anchor */
.tooltip {
position: fixed;
position-anchor: --my-anchor;
/* Place below the anchor, centered horizontally */
top: anchor(bottom);
left: anchor(center);
translate: -50% 8px;
}
/* Automatic fallback positioning when space is tight */
.tooltip {
position-try-options: flip-block, flip-inline;
}
The position-try-options feature is the killer capability: when the tooltip would overflow the viewport, the browser automatically tries alternative positions (flip to the other side, move to a different edge) without any JavaScript viewport calculations. This replaces hundreds of lines of collision detection logic.
Anchor positioning pairs beautifully with the Popover API and <dialog>. Since popovers render in the top layer (above all stacking contexts), anchor positioning tethers them back to their triggering element while they scroll together. Adam Argyle’s anchoreum.com is a comprehensive interactive playground for exploring anchor positioning patterns, and Michelle Barker (CSS { In Real Life }) demonstrates progressively enhanced popover toggletips that degrade to simple anchor links in unsupported browsers.
Resources:
- CSS Anchor Positioning API — Chrome Developers
- Anchor Positioning — web.dev
- Using CSS Anchor Positioning — MDN
- Anchoreum — Adam Argyle’s Anchor Positioning Playground
- Anchor Positioning + Popover for JS-Free Menus — CSS { In Real Life }
- CSS Tooltip System with Popover + Anchor — modern-css.com
- Popover + Anchor Positioning Polyfills — OddBird
Popover API + interestfor: Declarative Show/Hide Without JavaScript
The Popover API (popover attribute) provides declarative show/hide behavior for overlays, tooltips, dropdowns, and dialogs — no JavaScript event listeners required. Combined with the new interestfor attribute (Chrome 135+) for hover/focus triggering, and commandfor for click triggering, you can build complex interactive components entirely in HTML and CSS:
<!-- Click-triggered popover -->
<button commandfor="menu" command="toggle-popover">Menu</button>
<div id="menu" popover>
<nav><!-- menu content --></nav>
</div>
<!-- Hover/focus-triggered tooltip (Chrome 135+) -->
<button id="help-btn" interestfor="help-tip">Help</button>
<div id="help-tip" popover="hint">
Helpful tooltip text
</div>
Popovers render in the top layer, meaning they’re guaranteed to be visible above all other content regardless of z-index and stacking context complexity. Light-dismiss (clicking outside closes the popover), keyboard navigation (Escape to close), and focus management are all handled automatically by the browser.
The three popover types serve different purposes: popover="auto" (default, light-dismiss, only one open at a time), popover="manual" (must be explicitly closed, multiple can be open), and popover="hint" (for tooltips — opening a hint closes other hints but doesn’t close auto popovers). Entry/exit animations work with @starting-style and transition-behavior: allow-discrete:
[popover] {
opacity: 0;
translate: 0 10px;
transition: opacity 0.3s, translate 0.3s, display 0.3s;
transition-behavior: allow-discrete;
&:popover-open {
opacity: 1;
translate: 0 0;
}
@starting-style {
&:popover-open {
opacity: 0;
translate: 0 10px;
}
}
}
Resources:
- Popover API — MDN
- Beautiful
<details>— Adam Argyle (nerdy.dev) - popover=hint — Chrome Developers
- Popover API Accessibility — Alexander Lehner
Scroll-Driven Animations: GSAP-Level Effects in Pure CSS
Scroll-driven animations, the most transformative CSS animation feature since keyframes, allow you to control standard CSS @keyframes animations with scroll position instead of time. This replaces libraries like GSAP ScrollTrigger, ScrollMagic, and Locomotive Scroll for the majority of scroll-linked effects — parallax, reveal-on-scroll, progress indicators, sticky headers, and immersive storytelling.
There are two animation timeline types:
scroll() ties the animation to the scroll position of a scrollable container — like a progress bar that fills as you scroll down the page:
/* Reading progress indicator */
.progress-bar {
position: fixed;
top: 0;
left: 0;
height: 3px;
background: var(--accent);
transform-origin: left;
animation: grow-width linear both;
animation-timeline: scroll(); /* Tied to root scroller */
}
@keyframes grow-width {
from { transform: scaleX(0); }
to { transform: scaleX(1); }
}
view() ties the animation to an element’s visibility within the viewport — triggering as elements enter and exit the scroll port:
/* Fade-in on scroll with reduced motion respect */
.card {
@media (prefers-reduced-motion: no-preference) {
animation: fade-slide-in linear both;
animation-timeline: view();
animation-range: entry 0% entry 100%;
}
}
@keyframes fade-slide-in {
from {
opacity: 0;
transform: translateY(50px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
The animation-range property controls which portion of the scroll triggers the animation: entry (element entering viewport), exit (leaving), cover (entire time visible), contain (fully visible period). Bramus Van Damme (Chrome DevRel) built the essential Scroll-Driven Animations Range Visualizer for experimenting with ranges interactively.
Adam Argyle’s Scroll Driven Animations Notebook (nerdy.dev) contains 30+ examples ranging from simple slide-ins to complex 3D spatial zoom effects and synchronized swimmers — all pure CSS. As CSS-Tricks notes, “it’s easy to drop your jaw at Adam’s demos, but remember we’re still working with plain old CSS animations — the difference is the timeline they’re on.”
Browser support: Chrome 115+, Edge 115+. Firefox and Safari are in progress but lagging. This is an ideal progressive enhancement — wrap in @media (prefers-reduced-motion: no-preference) and @supports (animation-timeline: scroll()), and unsupported browsers simply don’t animate. A polyfill is available for broader reach.
Always respect prefers-reduced-motion. Scroll-driven animations can cause motion sickness in sensitive users. Wrap all scroll animations in @media (prefers-reduced-motion: no-preference).
Resources:
- Scroll-Driven Animations Notebook — Adam Argyle (nerdy.dev)
- Practical Introduction to Scroll-Driven Animations — Adam Argyle, Codrops
- Scroll-Driven Animations Range Visualizer — Bramus Van Damme
- Start Using Scroll-Driven Animations Today — Cyd Stumpel
- Scroll-Driven Animations Notebook — CSS-Tricks
- Adam Argyle’s Scroll-Driven Animation CodePen Collection
- Scroll-Driven Animations Polyfill — GitHub
View Transitions API: Page-Level Animations Without Frameworks
The View Transitions API provides smooth animated transitions between different states of a page (SPA) or between entirely different pages (MPA), replacing the need for frameworks like Framer Motion, React Transition Group, or GSAP page-level animations. Jake Archibald (who proposed the original API) and Bramus Van Damme (who built the comprehensive demo site at view-transitions.chrome.dev) are the primary authors of this feature.
Same-document (SPA) view transitions — wrap a DOM update in document.startViewTransition() and the browser captures a screenshot of the old state, applies your DOM changes, then animates between old and new using CSS animations on pseudo-elements:
// In your SPA router or state change handler
document.startViewTransition(() => {
// Update the DOM
updateView(newData);
});
/* Customize the default crossfade */
::view-transition-old(root) {
animation: fade-out 0.3s ease;
}
::view-transition-new(root) {
animation: fade-in 0.3s ease;
}
/* Animate specific elements between states */
.product-image {
view-transition-name: hero-image;
}
::view-transition-group(hero-image) {
animation-duration: 0.4s;
}
Cross-document (MPA) view transitions — enabled with a single CSS rule, no JavaScript required. This is the truly revolutionary capability: native page transitions across full server-rendered navigations:
@view-transition {
navigation: auto;
}
Add view-transition-name to elements that should morph between pages (e.g., a product thumbnail on a listing page morphing into the hero image on the product page). The browser handles snapshot capture, cross-fade, and geometry interpolation automatically.
Browser support: Same-document transitions are in Chrome 111+, Safari 18+, Firefox 144+. Cross-document transitions are in Chrome 126+, Safari 18.2+. The view-transition-class property (Chrome 125+, Safari 18.2+, Firefox 144+) enables applying the same transition styles to multiple elements without unique names.
Bramus’s demo site at view-transitions.chrome.dev showcases dozens of patterns: pagination slides, stack navigators, card list add/remove animations, video player expansions, circular clip-path reveals, and more — all categorized by SPA vs MPA usage.
Resources:
- View Transitions API — MDN
- Bramus’s View Transitions Demo Collection
- MPA View Transitions Deep Dive — Bramus Van Damme
- Smooth Transitions with the View Transition API — Chrome Developers
- View Transition API Part 2 — Smashing Magazine
- Practical Guide to View Transitions — Cyd Stumpel
- View Transitions Practical Guide — Bejamas
interpolate-size & calc-size(): Finally Animate to height: auto
Animating to height: auto has been one of the most requested CSS features for nearly two decades. Every accordion, collapsible panel, and expand/collapse pattern required either JavaScript measurement (getBoundingClientRect()), max-height hacks (with inaccurate animation timing), or the FLIP technique. As of Chrome 129+, interpolate-size and calc-size() solve this natively.
interpolate-size: allow-keywords is the simple, set-and-forget approach. Add it to :root and the browser can transition between any <length-percentage> value and intrinsic sizing keywords (auto, min-content, max-content, fit-content):
/* Add to your CSS reset — enables smooth auto-height animation sitewide */
:root {
interpolate-size: allow-keywords;
}
/* Now this just works! */
.accordion-content {
height: 0;
overflow: clip;
transition: height 0.35s ease;
}
.accordion[open] .accordion-content {
height: auto;
}
Josh W. Comeau’s tutorial on interpolate-size (March 2025) recommends adding it to your global CSS reset and treating it as a progressive enhancement: unsupported browsers still toggle between states, just without animation. Bramus Van Damme’s Chrome Developers article provides the definitive reference with interactive demos, including <details> element animation.
calc-size() is the power tool — it enables calculations on intrinsic sizes, which interpolate-size alone cannot do:
/* Half the element's auto width */
width: calc-size(auto, size * 0.5);
/* Auto height minus 10 pixels */
height: calc-size(auto, size - 10px);
/* Animate between fit-content and 70% of fit-content */
@keyframes narrow {
from { width: fit-content; }
to { width: calc-size(fit-content, size * 0.7); }
}
The size keyword represents the resolved intrinsic value. Using calc-size() automatically applies interpolate-size: allow-keywords, so it’s all you need for animation.
The ::details-content pseudo-element (Chrome 131+) enables styling the expandable content of <details> elements directly, making native HTML accordions smoothly animatable without wrapper elements.
Browser support: Chrome/Edge 129+ only as of early 2026. Progressive enhancement is perfect here — the feature degrades to instant toggling. Andy Bell (Piccalilli) calls interpolate-size “a great example of progressive enhancement” — it doesn’t break anything when unsupported.
Resources:
- Animate to height: auto in CSS — Bramus Van Damme, Chrome Developers
- Keyword Transitions: Animate height: 0 to height: auto — Josh W. Comeau
- interpolate-size — MDN
- calc-size() — CSS-Tricks Almanac
- calc-size() and interpolate-size — 12 Days of Web
- interpolate-size as Progressive Enhancement — Andy Bell, Piccalilli
Compositor-Safe Animations & the FLIP Technique
Not all CSS properties are created equal when it comes to animation performance. Animating width, height, top, left, margin, or padding triggers layout recalculation for the animated element and potentially its siblings and ancestors — expensive work on the main thread that causes jank on mid-range devices. Compositor-safe properties — transform, opacity, and filter — can be animated entirely on the GPU compositor thread, independent of main thread activity.
/* ❌ Triggers layout on every frame */
.bad { transition: width 0.3s, left 0.3s; }
/* ✅ Compositor-only — smooth 60fps even during heavy JS execution */
.good { transition: transform 0.3s, opacity 0.3s; }
The FLIP technique (First, Last, Invert, Play), coined by Paul Lewis and popularized by Sarah Drasner, bridges the gap when you need layout-triggering changes to appear animated. Instead of animating the layout property directly, you:
- First: Record the element’s initial position (
getBoundingClientRect()) - Last: Apply the DOM/style change (element snaps to new position)
- Invert: Calculate the delta and apply a
transformthat moves the element back to its original position - Play: Remove the transform with a transition, animating from “old position” to “new position” using only
transform
// FLIP pattern
const el = document.querySelector('.card');
const first = el.getBoundingClientRect();
// Apply the layout change
el.classList.add('expanded');
const last = el.getBoundingClientRect();
const deltaX = first.left - last.left;
const deltaY = first.top - last.top;
// Invert: snap back to old position via transform
el.style.transform = `translate(${deltaX}px, ${deltaY}px)`;
el.style.transition = 'none';
// Play: animate to new position by removing the transform
requestAnimationFrame(() => {
el.style.transition = 'transform 0.3s ease';
el.style.transform = '';
});
The View Transitions API effectively automates the FLIP technique at the browser level — it captures snapshots, calculates geometry differences, and animates using compositor-safe transforms. For most cases where you’d previously hand-code FLIP, document.startViewTransition() is the modern replacement.
SVG animation gotcha: SVG elements that use transform attributes (not CSS transform) do not animate on the compositor. Ensure SVG animations use CSS transform property rather than the SVG transform attribute. Additionally, will-change: transform promotes an element to its own compositor layer — use it sparingly on elements you know will animate, as over-promotion wastes GPU memory.
Resources:
- FLIP Your Animations — Paul Lewis
- Animations Guide — web.dev
- Compositor-Only Properties — Chrome Developers
- CSS Triggers — What Gets Recalculated
Scroll-State Container Queries: CSS-Only Sticky/Snapped/Overflowing Detection
An emerging capability (Chrome experimental) extends container queries to query scroll-related states — whether an element is stuck (via position: sticky), snapped (via scroll-snap), or whether a container is overflowing. This replaces IntersectionObserver and scroll event listeners for common scroll-responsive UI patterns.
/* Style a sticky header when it's actually stuck */
.sticky-header {
container-type: scroll-state;
position: sticky;
top: 0;
}
@container scroll-state(stuck: top) {
.sticky-header {
background: var(--surface-elevated);
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
}
/* Show scroll affordances only when content overflows */
.scroll-container {
container-type: scroll-state;
}
@container scroll-state(scrollable: top) {
.scroll-shadow-top { opacity: 1; }
}
Adam Argyle demonstrated at CSS Day 2025 how scroll-state() queries enable CSS-only carousels with state-aware navigation, snapped item indicators, and scroll affordance shadows — all without JavaScript.
Combined with ::scroll-marker and ::scroll-button() pseudo-elements (also in development), these features aim to make carousel/scroller patterns achievable entirely in CSS, replacing libraries like Swiper, Flickity, and Embla Carousel for many use cases.
Resources:
- Scroll-State Container Queries — Chrome Developers
- CSS Carousel Demo — Chrome
- Adam Argyle CSS Day 2025 — Scroll Experiences
@starting-style & Discrete Animations: Animate Entry/Exit from display: none
Previously impossible: animating an element’s appearance from display: none to display: block. CSS couldn’t transition discrete properties like display — elements would snap instantly. @starting-style (Chrome 117+, Safari 17.5+, Firefox 129+) and transition-behavior: allow-discrete solve this:
.modal {
opacity: 0;
scale: 0.95;
display: none;
transition: opacity 0.3s, scale 0.3s, display 0.3s;
transition-behavior: allow-discrete;
&[open] {
opacity: 1;
scale: 1;
display: block;
@starting-style {
opacity: 0;
scale: 0.95;
}
}
}
@starting-style defines the from state when an element first becomes visible (enters the DOM, changes from display: none, or opens as a popover). transition-behavior: allow-discrete allows display and overlay to participate in transitions. Together, they enable smooth entry and exit animations for dialogs, popovers, tooltips, and conditionally rendered elements — patterns that previously required React Transition Group, Framer Motion, or manual requestAnimationFrame choreography.
This is particularly powerful with the Popover API: all entry/exit animations can be defined in pure CSS with @starting-style, no JavaScript animation libraries needed.
Resources:
- @starting-style — MDN
- Transition from display: none — Chrome Developers
- Four New CSS Features for Entry/Exit Animations — Chrome Developers
Putting It All Together: The JavaScript Libraries You May No Longer Need
The cumulative impact of these CSS features is staggering. Consider what previously required JavaScript:
| Use Case | Was JavaScript | Now CSS/HTML |
|---|---|---|
| Tooltip positioning | Floating UI, Tippy.js, Popper.js | CSS Anchor Positioning |
| Show/hide overlays | Custom JS event handlers | Popover API + commandfor/interestfor |
| Scroll-linked animation | GSAP ScrollTrigger, ScrollMagic | animation-timeline: scroll()/view() |
| Page transitions | Framer Motion, Barba.js | View Transitions API |
| Animate height: auto | JS measurement + FLIP | interpolate-size: allow-keywords |
| Entry/exit animations | React Transition Group | @starting-style + allow-discrete |
| Component responsiveness | ResizeObserver | Container Queries |
| Parent-based styling | JS state management | :has() |
| Stuck/snapped detection | IntersectionObserver | Scroll-State Container Queries |
| Carousel indicators | Swiper, Flickity | ::scroll-marker + scroll-state() |
| Masonry layout | Masonry.js, Isotope | CSS grid-template-rows: masonry / grid-lanes |
CSS native masonry is arriving via two competing proposals: grid-template-rows: masonry (the original approach) and the newer grid-lanes proposal, which introduces masonry as a distinct layout mode rather than overloading Grid. Both aim to eliminate JavaScript masonry libraries (Masonry.js, Isotope, Colcade) for Pinterest-style layouts where items have varying heights. The grid-lanes approach is gaining momentum as it avoids the conceptual conflicts of mixing masonry semantics into Grid’s two-dimensional model.
This doesn’t mean JavaScript animation libraries are dead — GSAP, Framer Motion, and others remain essential for complex choreographed sequences, physics-based animations, and timeline coordination that pure CSS can’t express. But for the 80% of common UI patterns — reveal-on-scroll, expand/collapse, tooltip positioning, page transitions — CSS now does it better, faster, and with zero JavaScript cost.
The progressive enhancement principle applies everywhere: these features degrade gracefully. Unsupported browsers skip the animation but still show the content. Always wrap motion in @media (prefers-reduced-motion: no-preference). Always test with real devices, not just desktop Chrome.
Resources:
- Adam Argyle — nerdy.dev (CSS experiments, notebooks, and demos)
- Bramus Van Damme — bram.us (View Transitions, Scroll-Driven Animations)
- Josh W. Comeau — joshwcomeau.com (interactive CSS tutorials)
- Ahmad Shadeed — ishadeed.com (container queries, layout, visual CSS)
- Michelle Barker — css-irl.info (anchor positioning, modern CSS experiments)
- Una Kravets — web.dev / Chrome DevRel (CSS features, container queries)
- Andy Bell — piccalil.li (progressive enhancement, CSS architecture)
- Kevin Powell — YouTube / kevinpowell.co (CSS tutorials)
- CSS Wrapped 2024/2025 — Chrome DevRel
- State of CSS 2025 Survey Results
- What’s New in CSS 2026 — modern-css.com
- Interop 2025 Dashboard — web.dev