Web Development · CSS
CSS View Transitions and Scroll-Driven Animations
The View Transitions API and scroll-driven animations are now baseline in every major browser. Here's what they replace and working code for both.
Anurag Verma
7 min read
Sponsored
For years, adding page transitions meant choosing a library. Framer Motion for React. GSAP for anything JavaScript-heavy. Barba.js if you wanted to feel like a web veteran from 2018. All of them work well, but they also add kilobytes to your bundle and introduce a layer between you and what the browser already knows how to do.
Two browser APIs (the View Transitions API and scroll-driven animations) now have consistent support across Chrome, Firefox, and Safari. They don’t replace every animation use case, but for a large subset of what web developers reach for libraries to accomplish, they’re the simpler choice.
View Transitions: animated page changes without a framework
The View Transitions API animates between two DOM states, including a full page navigation, without an animation library. Wrap the change in document.startViewTransition(), and the browser captures a screenshot of the current state, applies your DOM update, then cross-fades between the two automatically. Style the transition further with the ::view-transition-old() and ::view-transition-new() pseudo-elements if the default crossfade isn’t what you want.
The Basics
Wrap a DOM change in document.startViewTransition():
document.startViewTransition(() => {
// Make your DOM change here
document.getElementById('content').innerHTML = newContent
})
The browser captures a screenshot of the current state, makes the change, then animates between the two. By default it crossfades. That’s it for the most basic usage.
Customizing the Animation
The captured elements get pseudo-elements that you can style with CSS:
/* The exiting content */
::view-transition-old(root) {
animation: 300ms ease-out both fade-out;
}
/* The entering content */
::view-transition-new(root) {
animation: 300ms ease-in both fade-in;
}
@keyframes fade-out {
to { opacity: 0; }
}
@keyframes fade-in {
from { opacity: 0; }
}
You can target individual elements by name instead of the whole page:
.product-card {
view-transition-name: product-card;
}
::view-transition-old(product-card) {
animation: 400ms ease-out slide-out-left;
}
::view-transition-new(product-card) {
animation: 400ms ease-in slide-in-right;
}
This gives you element-level continuity across DOM changes. If a card exists before and after the transition, the browser morphs it smoothly from its old position to its new one.
Page Transitions in a Multi-Page App (MPA)
Until recently, the View Transitions API only worked for single-page apps where JavaScript controlled navigation. The @view-transition CSS rule changes this:
@view-transition {
navigation: auto;
}
Add that rule to every page and the browser handles cross-document transitions automatically. No JavaScript required. Navigation between pages that share this declaration will cross-fade by default.
For custom per-route animations:
/* On the page you're leaving */
@keyframes slide-out {
to { transform: translateX(-100%); }
}
/* On the page you're entering */
@keyframes slide-in {
from { transform: translateX(100%); }
}
@media (prefers-reduced-motion: no-preference) {
::view-transition-old(root) {
animation: slide-out 300ms ease-in;
}
::view-transition-new(root) {
animation: slide-in 300ms ease-out;
}
}
The prefers-reduced-motion wrapper is not optional. Users who have asked for reduced motion in their OS settings need to be respected.
Browser Support in 2026
The View Transitions API for same-document transitions has been in Chrome since version 111 (March 2023). Firefox shipped it in version 130 (August 2024). Safari added it in Safari 18 (September 2024). Cross-document transitions (the @view-transition rule) followed shortly after in all three.
By May 2026, all three major browsers support both modes, covering over 95% of global traffic.
Scroll-Driven Animations
Scroll-driven animations let you tie CSS animations to scroll position rather than time. Previously this required JavaScript scroll event listeners or an IntersectionObserver. Now it’s a CSS-only feature.
The Two Scroll Timeline Types
Scroll timeline: tied to a scrollable container. The animation progresses as the user scrolls.
View timeline: tied to an element’s position in the viewport. The animation progresses as the element enters and exits the visible area.
Animating as the Page Scrolls
@keyframes progress-bar {
from { transform: scaleX(0); }
to { transform: scaleX(1); }
}
.reading-progress {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 4px;
background: #0066ff;
transform-origin: left;
animation: progress-bar linear;
animation-timeline: scroll(root);
animation-fill-mode: both;
}
scroll(root) creates a scroll timeline tied to the document root. The .reading-progress bar scales from 0 to full width as the user scrolls the page from top to bottom. No JavaScript.
Animating Elements as They Enter the Viewport
@keyframes fade-up {
from {
opacity: 0;
transform: translateY(32px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.reveal-on-scroll {
animation: fade-up ease-out both;
animation-timeline: view();
animation-range: entry 0% entry 50%;
}
view() creates a view timeline for the element. animation-range: entry 0% entry 50% means the animation plays during the first half of the element entering the viewport. Once it’s 50% visible, the animation is complete.
This replaces a common pattern that used to require:
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('visible')
observer.unobserve(entry.target)
}
})
}, { threshold: 0.1 })
document.querySelectorAll('.reveal-on-scroll').forEach(el => observer.observe(el))
The CSS version is shorter and more declarative. The JavaScript version gives you more control when you need it.
Parallax Without JavaScript
@keyframes parallax-shift {
from { transform: translateY(-20%); }
to { transform: translateY(20%); }
}
.parallax-image {
animation: parallax-shift linear;
animation-timeline: scroll(root);
animation-range: 0% 100%;
}
This shifts the image vertically as the user scrolls. For backgrounds and decorative elements, it creates a parallax effect without a JavaScript library.
Browser Support
Scroll-driven animations shipped in Chrome 115 (July 2023). Firefox shipped them in version 133 (November 2024). Safari added support in Safari 18.2 (December 2024).
By 2026, support is consistent across all major browsers. One practical note: Safari has had some edge-case inconsistencies with animation-range timing. Test on Safari if you’re using complex range combinations.
Pairing the Two APIs
The real power comes when you combine both. An element transitions via the View Transitions API when the URL changes, while other elements animate in based on scroll position as the user reads the page.
// Intercept link clicks for view transitions
document.addEventListener('click', (e) => {
const link = e.target.closest('a[href]')
if (!link || link.href.startsWith('mailto')) return
if (!document.startViewTransition) return // graceful degradation
e.preventDefault()
document.startViewTransition(async () => {
const response = await fetch(link.href)
const html = await response.text()
const parser = new DOMParser()
const doc = parser.parseFromString(html, 'text/html')
document.querySelector('main').replaceWith(doc.querySelector('main'))
history.pushState({}, '', link.href)
})
})
The page content morphs when navigating. Then scroll-driven animations handle the individual elements appearing as the user scrolls the new page.
What These APIs Don’t Cover
Both APIs work well for transitions tied to navigation and scroll. They don’t handle:
- Complex interaction-driven animations (hover states with spring physics, drag gestures, gesture-based transitions). Framer Motion and GSAP are still the right tools here.
- Choreographed sequences where you need timeline control across many elements. GSAP’s
gsap.timeline()has no CSS equivalent. - Canvas or WebGL animations. Nothing in this spec touches those.
For straightforward page transitions and scroll-reveal effects, the native APIs are now good enough. For complex, orchestrated motion, reach for GSAP or Framer Motion. The two aren’t in competition. They solve different scopes of the same general problem.
The Practical Upshot
If you’re building a content site, marketing page, or any MPA in 2026, the @view-transition { navigation: auto } rule deserves to be in your baseline CSS. It takes 10 seconds to add and immediately makes navigation feel more polished.
Scroll-driven animations replace a common use of IntersectionObserver with declarative CSS. For reveal effects and reading progress bars, the CSS version is less code and easier to maintain, and it’s one more input worth weighing alongside layout shift and load timing in your Core Web Vitals work.
Start with the defaults. Customize when the defaults aren’t enough. Add libraries when the spec’s limits become the bottleneck.
Frequently asked questions
- What does the View Transitions API do?
- It animates the change between two DOM states, a single-page app route change or a full multi-page navigation, by capturing the before and after states and cross-fading (or custom-animating) between them. You trigger it with document.startViewTransition() for JS-driven changes, or the @view-transition CSS rule for plain page navigations, with no animation library involved.
- Do scroll-driven animations require JavaScript?
- No. Setting animation-timeline: scroll(root) or animation-timeline: view() on an element ties its existing CSS @keyframes animation to scroll position instead of time. This replaces the common IntersectionObserver pattern used for scroll-reveal effects and reading-progress bars with pure CSS.
- Are View Transitions and scroll-driven animations supported in all browsers?
- Yes, as of 2026. Chrome shipped View Transitions in version 111 (March 2023) and scroll-driven animations in 115 (July 2023). Firefox and Safari added both within the following 18 months. Combined, they now cover over 95% of global browser traffic.
- Should I replace GSAP or Framer Motion with these native APIs?
- Only for the use cases they cover: page transitions and scroll-reveal effects. For complex interaction-driven animation, hover states with spring physics, drag gestures, or multi-element choreographed timelines, GSAP and Framer Motion still do things native CSS can't. The two approaches aren't competing, they cover different scopes of the same problem.
Sponsored
More from this category
More from Web Development
Sponsored
Discussion
Join the conversation.
Comments are powered by GitHub Discussions. Sign in with your GitHub account to leave a comment.
Sponsored