Scroll-Driven Animations
Scroll-driven animations are the most-requested AI CSS output — elements that animate as the user scrolls. CSS now has two native approaches: the classic Intersection Observer pattern (JS) and the new pure-CSS animation-timeline.
1. The Classic Pattern: Intersection Observer (Works Everywhere)
Before native scroll animations, JavaScript's Intersection Observer was the gold standard. It's still the most compatible approach:
/* 1. Start elements hidden and positioned for animation */
.animate-on-scroll {
opacity: 0;
transform: translateY(30px);
transition: opacity 0.7s ease, transform 0.7s ease;
}
/* 2. This class is added by JS when element enters viewport */
.animate-on-scroll.is-visible {
opacity: 1;
transform: translateY(0);
}
/* 3. Stagger with CSS variables */
.animate-on-scroll { transition-delay: calc(var(--delay, 0) * 100ms); }
// Minimal Intersection Observer setup
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add('is-visible');
observer.unobserve(entry.target); // Animate once only
}
});
},
{ threshold: 0.15 }
);
document.querySelectorAll('.animate-on-scroll').forEach((el, i) => {
el.style.setProperty('--delay', i); // Stagger delay
observer.observe(el);
});
2. Common Scroll Animation Variants
/* Fade in from below */
@keyframes fade-up {
from { opacity: 0; transform: translateY(30px); }
to { opacity: 1; transform: translateY(0); }
}
/* Fade in from left */
@keyframes fade-left {
from { opacity: 0; transform: translateX(-40px); }
to { opacity: 1; transform: translateX(0); }
}
/* Fade in from right */
@keyframes fade-right {
from { opacity: 0; transform: translateX(40px); }
to { opacity: 1; transform: translateX(0); }
}
/* Scale pop */
@keyframes scale-pop {
from { opacity: 0; transform: scale(0.85); }
to { opacity: 1; transform: scale(1); }
}
/* Apply via class */
.fade-up { animation: fade-up 0.7s ease both; }
.fade-left { animation: fade-left 0.7s ease both; }
.fade-right { animation: fade-right 0.7s ease both; }
.scale-pop { animation: scale-pop 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) both; }
3. Native CSS: animation-timeline: scroll()
Modern browsers now support scroll-driven animations natively — no JavaScript required:
/* Scroll progress bar — fills as you scroll the page */
.scroll-progress {
position: fixed;
top: 0;
left: 0;
height: 3px;
width: 100%;
background: linear-gradient(90deg, var(--color-primary), var(--color-secondary));
transform-origin: left;
animation: scroll-progress linear;
animation-timeline: scroll(root); /* scroll() = root viewport by default */
}
@keyframes scroll-progress {
from { transform: scaleX(0); }
to { transform: scaleX(1); }
}
4. view() Timeline — Element-Linked Scroll Animation
The view() timeline links the animation to when an element enters/exits the viewport:
.card {
animation: fade-up-in linear both;
animation-timeline: view();
animation-range: entry 0% entry 40%; /* Animate during first 40% of entry */
}
@keyframes fade-up-in {
from { opacity: 0; transform: translateY(40px); }
to { opacity: 1; transform: translateY(0); }
}
animation-range Values
/* During entry into viewport — most common */
animation-range: entry 0% entry 50%;
/* During exit from viewport */
animation-range: exit 0% exit 100%;
/* Cover entire visibility (entry → exit) */
animation-range: contain 0% contain 100%;
/* Reverse on exit */
animation-range: entry 0% exit 100%;
5. Parallax Effect — Pure CSS
@keyframes parallax-bg {
from { transform: translateY(0); }
to { transform: translateY(-20%); }
}
.hero-background {
animation: parallax-bg linear;
animation-timeline: scroll(root);
}
/* Foreground moves slightly slower */
@keyframes parallax-fg {
from { transform: translateY(0); }
to { transform: translateY(-8%); }
}
.hero-content {
animation: parallax-fg linear;
animation-timeline: scroll(root);
}
6. Sticky Progress Indicator (Section Counter)
/* Highlight active section in nav based on scroll position */
.section {
view-timeline-name: --section;
view-timeline-axis: block;
}
.nav-link {
animation: activate linear both;
animation-timeline: --section;
animation-range: contain;
}
@keyframes activate {
0%, 100% { color: var(--color-text-muted); }
50% { color: var(--color-primary); font-weight: 600; }
}
7. Browser Support and Fallback Strategy
| Feature | Chrome | Firefox | Safari | Support |
|---|---|---|---|---|
animation-timeline: scroll() | 115+ | 110+ | ❌ | ~82% |
animation-timeline: view() | 115+ | 110+ | ❌ | ~82% |
| Intersection Observer | All | All | All | ~99% |
/* Always provide a no-animation fallback */
@media (prefers-reduced-motion: reduce) {
.animate-on-scroll,
[style*="animation-timeline"] {
animation: none !important;
opacity: 1 !important;
transform: none !important;
}
}
/* Feature-detect native scroll animations */
@supports (animation-timeline: scroll()) {
.scroll-anim { animation: fade-up linear both; animation-timeline: view(); }
}
/* Fallback using Intersection Observer for browsers without support */
8. AI Prompt for Scroll Animations
Generate a scroll-triggered animation for a list of .feature-card elements.
- Use Intersection Observer for maximum browser compatibility.
- Cards should fade in from below with a stagger of 100ms each.
- Use CSS custom properties for token values:
--color-primary: hsl(245, 80%, 62%)
--ease: 0.6s cubic-bezier(0.16, 1, 0.3, 1)
- Include prefers-reduced-motion media query.
- Output: vanilla JS + CSS (no libraries).