scroll-animations
ساخت انیمیشنهای روان و دسترسپذیر مبتنی بر اسکرول برای رابطهای وب
این Skill برای پیادهسازی انیمیشنهای scroll-triggered و scroll-linked در رابطهای وب استفاده میشود؛ از reveal و stagger در زمان ورود به viewport تا parallax، progress indicator و scrub با موقعیت اسکرول. الگوهای آماده برای GSAP ScrollTrigger، Intersection Observer و CSS scroll-driven animations ارائه میکند.
با رعایت توکنهای حرکتی M3، بهینهسازی عملکرد و پشتیبانی از prefers-reduced-motion، تجربهای روان، قابلکنترل و دسترسپذیر میسازد. برای پروژههای Vanilla JS، React و SPA نیز الگوهای cleanup و مدیریت lifecycle دارد.
چه چیزی میگیرید
- الگوهای reveal، stagger و trigger با Intersection Observer و GSAP ScrollTrigger
- پیادهسازی parallax، progress indicator و انیمیشنهای scrubشونده
- مدیریت انیمیشنهای بالای fold برای بهبود perceived performance
- راهکارهای دسترسپذیری با `prefers-reduced-motion` و حذف حرکات پرخطر
- نکات بهینهسازی، استفاده محدود از `will-change` و cleanup در React/SPA
برای چه کاری خوب است
- ساخت landing pageها و وبسایتهای محصول با reveal و parallax کنترلشده
- پیادهسازی progress indicator یا scrub animation وابسته به اسکرول
- توسعه رابطهای React و SPA که به مدیریت lifecycle انیمیشن نیاز دارند
- طراحی تجربههای تعاملی دسترسپذیر با پشتیبانی از reduced motion
برای چه کاری نیست
- انیمیشنهای مستقل از اسکرول یا طراحی کامل سیستم موشن بدون تعامل اسکرولی
- پروژههایی که به horizontal scroll hijack، zoom شدید یا حرکات 3D بدون کنترل نیاز دارند
- صفحههایی که انیمیشن اسکرولی در آنها ارزش کاربردی ندارد یا عملکرد اولویت مطلق است
دربارهٔ این اسکیل
Scroll Animations
Scroll-triggered and scroll-linked motion patterns.
Standards Reference
| Tool/API | Documentation |
|---|---|
| GSAP ScrollTrigger | gsap.com/docs/v3/Plugins/ScrollTrigger |
| Intersection Observer | developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API |
| CSS scroll-driven | developer.mozilla.org/en-US/docs/Web/CSS/CSS_scroll-driven_animations |
Two Types of Scroll Animation
| Type | Behavior | Use Case |
|---|---|---|
| Scroll-triggered | Animation plays when element enters viewport | Reveal animations, stagger |
| Scroll-linked | Animation progress tied to scroll position | Parallax, scrubbing, progress |
Scroll-Triggered (Reveal on Scroll)
Intersection Observer (Vanilla JS)
const observer = new IntersectionObserver(
(entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('visible');
observer.unobserve(entry.target); // Once only
}
});
},
{ threshold: 0.2, rootMargin: '0px 0px -50px 0px' }
);
document.querySelectorAll('.animate-on-scroll').forEach(el => {
observer.observe(el);
});
.animate-on-scroll {
opacity: 0;
transform: translateY(20px);
transition: opacity 250ms cubic-bezier(0.05, 0.7, 0.1, 1),
transform 250ms cubic-bezier(0.05, 0.7, 0.1, 1);
}
.animate-on-scroll.visible {
opacity: 1;
transform: translateY(0);
}
@media (prefers-reduced-motion: reduce) {
.animate-on-scroll {
opacity: 1;
transform: none;
transition: none;
}
}
GSAP ScrollTrigger (Triggered)
import gsap from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
gsap.registerPlugin(ScrollTrigger);
gsap.from('.card', {
opacity: 0,
y: 30,
duration: 0.25, // medium-1
ease: 'cubic-bezier(0.05, 0.7, 0.1, 1)', // emphasized-decelerate
stagger: 0.05,
scrollTrigger: {
trigger: '.card-grid',
start: 'top 80%', // When top of trigger hits 80% of viewport
toggleActions: 'play none none none'
}
});
Scroll-Linked (Scrubbing)
GSAP ScrollTrigger (Scrub)
gsap.to('.progress-bar', {
scaleX: 1,
ease: 'none', // Linear for scroll-linked
scrollTrigger: {
trigger: 'body',
start: 'top top',
end: 'bottom bottom',
scrub: true // Links animation to scroll position
}
});
Parallax
gsap.to('.parallax-bg', {
y: -100,
ease: 'none',
scrollTrigger: {
trigger: '.hero',
start: 'top top',
end: 'bottom top',
scrub: true
}
});
Viewport-Aware Stagger
Prioritize above-the-fold content for faster perceived load.
const cards = gsap.utils.toArray('.card');
// Determine which cards are above the fold
const foldPosition = window.innerHeight;
const aboveFold = cards.filter(card =>
card.getBoundingClientRect().top < foldPosition
);
const belowFold = cards.filter(card =>
card.getBoundingClientRect().top >= foldPosition
);
// Phase 1: Above fold — faster, tighter stagger
gsap.from(aboveFold, {
opacity: 0,
y: 16,
duration: 0.2, // short-4
ease: 'cubic-bezier(0.05, 0.7, 0.1, 1)',
stagger: 0.04 // Tighter
});
// Phase 2: Below fold — triggered on scroll
gsap.from(belowFold, {
opacity: 0,
y: 16,
duration: 0.25, // medium-1
ease: 'cubic-bezier(0.05, 0.7, 0.1, 1)',
stagger: 0.05,
scrollTrigger: {
trigger: belowFold[0],
start: 'top 85%'
}
});
Reduced Motion
Critical: Scroll animations often trigger vestibular issues.
const prefersReduced = window.matchMedia(
'(prefers-reduced-motion: reduce)'
).matches;
if (prefersReduced) {
// Option 1: Disable all scroll animations
ScrollTrigger.getAll().forEach(st => st.kill());
gsap.set('.animate', { opacity: 1, y: 0 });
// Option 2: Keep reveals, remove parallax
// Reveals are OK (fade only), parallax is not
}
Safe vs. Unsafe Scroll Animations
| Safe (Reduced Motion OK) | Unsafe (Disable) |
|---|---|
| Fade on scroll | Parallax |
| Progress indicator | Horizontal scroll hijack |
| Simple reveal | Zoom on scroll |
| Sticky headers | 3D transforms |
Performance
- Use
will-changesparingly - Animate transforms/opacity only — avoid layout properties
- Use
scrub: truenotscrub: 0.5for smoother scrubbing - Batch observers — one observer for multiple elements
- Kill ScrollTriggers on unmount (React/SPA)
// React cleanup
useEffect(() => {
const ctx = gsap.context(() => {
// ScrollTrigger animations here
});
return () => ctx.revert(); // Cleanup
}, []);
See references/scrolltrigger-patterns.md for advanced GSAP patterns.
See references/intersection-observer.md for vanilla JS patterns.
See references/scrubbing-media.md for Lottie/video scrubbing.
See references/parallax.md for parallax with accessibility.