Animation Architecture
The Adosa project uses:
GSAP 3.14.2 - Industry-standard animation library
ScrollTrigger - Scroll-based animation triggers
Lenis 1.3.16 - Smooth scrolling (desktop only)
All animations are coordinated through the page-ready event to ensure proper timing after page load.
Text Reveal Animation
The textRevealAnimation() utility creates elegant swipe-up text reveals.
Location: src/utils/textRevealAnimation.ts
Basic Usage
< div class = "content-section" >
< h2 class = "text-reveal" > Heading appears first </ h2 >
< p class = "text-reveal" > Paragraph follows with stagger </ p >
< p class = "text-reveal" > Another paragraph </ p >
</ div >
< script >
import { initTextRevealAnimation } from '../utils/textRevealAnimation' ;
window . addEventListener ( 'page-ready' , () => {
initTextRevealAnimation ( '.content-section' );
});
</ script >
Configuration Options
export interface TextRevealOptions {
selector ?: string ; // Element selector (default: ".text-reveal")
stagger ?: number ; // Delay between elements (default: 0.15s)
start ?: string ; // ScrollTrigger start (default: "top 85%")
duration ?: number ; // Animation duration (default: 1.2s)
}
Example with custom options:
initTextRevealAnimation ( '.hero-section' , {
selector: '.animate-text' ,
stagger: 0.2 ,
start: 'top 80%' ,
duration: 1.5
});
How It Works
// From src/utils/textRevealAnimation.ts:26-55
export function initTextRevealAnimation (
containerSelector : string ,
options : TextRevealOptions = {}
) {
const {
selector = ".text-reveal" ,
stagger = 0.15 ,
start = "top 85%" ,
duration = 1.2 ,
} = options ;
const elements = document . querySelectorAll (
` ${ containerSelector } ${ selector } `
);
elements . forEach (( element , index ) => {
gsap . to ( element , {
y: 0 , // Slide up from below
opacity: 1 , // Fade in
duration ,
ease: "power3.out" , // Smooth easing
scrollTrigger: {
trigger: element ,
start , // When to trigger
toggleActions: "play none none reverse" , // Play forward, reverse on scroll up
},
delay: index * stagger , // Stagger effect
});
});
}
Elements with .text-reveal class should have initial CSS state: .text-reveal {
opacity : 0 ;
transform : translateY ( 20 px );
}
This is typically defined in src/styles/animations.css.
Text Swipe Animation
The textSwipeAnimation() utility provides coordinated animations with custom delays.
Location: src/utils/textSwipeAnimation.ts
Basic Usage
< section class = "about-section" >
< h2 class = "text-swipe" > About Us </ h2 >
< p class = "text-swipe" data-delay = "0.2" > First paragraph </ p >
< p class = "text-swipe" data-delay = "0.4" > Second paragraph </ p >
</ section >
< script >
import { createTextSwipeAnimation } from '../utils/textSwipeAnimation' ;
window . addEventListener ( 'page-ready' , () => {
const section = document . querySelector ( '.about-section' );
if ( section ) {
createTextSwipeAnimation ( '.about-section .text-swipe' , section );
}
});
</ script >
Custom Delays
Use data-delay attribute for precise timing:
< h1 class = "text-swipe" data-delay = "0" > Immediate </ h1 >
< p class = "text-swipe" data-delay = "0.3" > Starts at 0.3s </ p >
< p class = "text-swipe" data-delay = "0.6" > Starts at 0.6s </ p >
Without data-delay, elements use automatic stagger (index × 0.15s).
How It Works
// From src/utils/textSwipeAnimation.ts:14-50
export function createTextSwipeAnimation (
selector : string ,
sectionTrigger : Element
) : ScrollTrigger | undefined {
const elements = document . querySelectorAll ( selector );
if ( elements . length === 0 ) return undefined ;
// Use a timeline to orchestrate all animations together
const tl = gsap . timeline ({
scrollTrigger: {
trigger: sectionTrigger ,
start: "top 50%" , // Trigger when section is 50% in view
toggleActions: "play none none reverse" , // Reversible animation
}
});
elements . forEach (( element , index ) => {
const el = element as HTMLElement ;
const customDelay = el . dataset . delay
? parseFloat ( el . dataset . delay )
: null ;
// Use custom delay if provided, otherwise use stagger
const startTime = customDelay !== null
? customDelay
: index * 0.15 ;
tl . to ( element , {
y: 0 ,
opacity: 1 ,
duration: 1.2 ,
ease: "power3.out"
}, startTime ); // Position in timeline
});
return tl . scrollTrigger ;
}
The key difference: textSwipeAnimation uses a timeline for coordinated control, while textRevealAnimation uses individual tweens. Use timeline when you need precise control over sequencing.
Fade-In Effect
< div class = "fade-section" >
< div class = "fade-item" > Content 1 </ div >
< div class = "fade-item" > Content 2 </ div >
</ div >
< script >
import gsap from 'gsap' ;
import { ScrollTrigger } from 'gsap/ScrollTrigger' ;
gsap . registerPlugin ( ScrollTrigger );
window . addEventListener ( 'page-ready' , () => {
gsap . utils . toArray ( '.fade-item' ). forEach (( item : any ) => {
gsap . from ( item , {
opacity: 0 ,
y: 50 ,
duration: 1 ,
scrollTrigger: {
trigger: item ,
start: 'top 80%' ,
toggleActions: 'play none none reverse' ,
}
});
});
});
</ script >
< div class = "parallax-section" >
< img src = "/image.jpg" class = "parallax-image" alt = "Background" />
< div class = "parallax-content" >
< h2 > Parallax Content </ h2 >
</ div >
</ div >
< style >
.parallax-section {
position : relative ;
height : 100 vh ;
overflow : hidden ;
}
.parallax-image {
position : absolute ;
top : 0 ;
left : 0 ;
width : 100 % ;
height : 120 % ; /* Extra height for parallax */
object-fit : cover ;
}
</ style >
< script >
import gsap from 'gsap' ;
import { ScrollTrigger } from 'gsap/ScrollTrigger' ;
gsap . registerPlugin ( ScrollTrigger );
window . addEventListener ( 'page-ready' , () => {
gsap . to ( '.parallax-image' , {
yPercent: 20 , // Move down 20%
ease: 'none' ,
scrollTrigger: {
trigger: '.parallax-section' ,
start: 'top top' ,
end: 'bottom top' ,
scrub: true , // Smooth scrubbing
}
});
});
</ script >
< div class = "scale-section" >
< div class = "scale-box" > Scales up on scroll </ div >
</ div >
< script >
import gsap from 'gsap' ;
import { ScrollTrigger } from 'gsap/ScrollTrigger' ;
gsap . registerPlugin ( ScrollTrigger );
window . addEventListener ( 'page-ready' , () => {
gsap . from ( '.scale-box' , {
scale: 0.8 ,
opacity: 0 ,
duration: 1 ,
scrollTrigger: {
trigger: '.scale-box' ,
start: 'top 75%' ,
end: 'top 25%' ,
scrub: 1 , // Scrub with 1 second lag
}
});
});
</ script >
Lenis provides buttery-smooth scrolling on desktop (disabled on mobile for native touch scrolling).
Location: src/layouts/BaseLayout.astro:74-108
Current Configuration
const lenis = new Lenis ({
duration: 0 , // Zero inertia (stops instantly)
easing : ( t ) => t , // Linear easing
orientation: "vertical" ,
gestureOrientation: "vertical" ,
smoothWheel: true ,
wheelMultiplier: 0.5 , // Halved scroll speed (premium feel)
});
Disable on Specific Elements
Prevent Lenis on certain elements: < div data-lenis-prevent >
<!-- Native scroll here -->
< div style = "overflow-y: auto; height: 300px;" >
Long content...
</ div >
</ div >
Stop Scroll Programmatically
Lenis is only enabled on desktop (>1024px). Mobile/tablet use native scroll for superior touch performance. See src/layouts/BaseLayout.astro:85-108.
1. Use will-change Strategically
Optimize elements that will animate:
.animated-element {
will-change : transform, opacity;
}
/* Remove after animation */
.animated-element.animation-complete {
will-change : auto ;
}
Don’t overuse will-change. It reserves GPU memory. Only use on elements that will definitely animate.
These properties are GPU-accelerated:
// ✅ Good (GPU accelerated)
gsap . to ( element , {
x: 100 , // transform: translateX()
y: 50 , // transform: translateY()
scale: 1.2 , // transform: scale()
opacity: 0.5 ,
});
// ❌ Avoid (triggers layout recalculation)
gsap . to ( element , {
width: '100px' ,
height: '200px' ,
top: '50px' ,
});
3. Use ease Appropriately
// Smooth natural motion
ease : "power3.out"
// Bouncy effect
ease : "elastic.out(1, 0.5)"
// Sharp start, smooth end
ease : "power4.inOut"
// Linear (for scrub animations)
ease : "none"
// ✅ Good: Create all ScrollTriggers together
window . addEventListener ( 'page-ready' , () => {
ScrollTrigger . batch ( '.animate-item' , {
onEnter : ( elements ) => {
gsap . to ( elements , {
opacity: 1 ,
y: 0 ,
stagger: 0.1
});
},
start: 'top 80%' ,
});
});
// ❌ Avoid: Creating ScrollTriggers in a loop
elements . forEach (( el ) => {
gsap . to ( el , {
scrollTrigger: { trigger: el },
opacity: 1
});
});
// After dynamic content loads
ScrollTrigger . refresh ();
// After window resize (debounced)
let resizeTimer ;
window . addEventListener ( 'resize' , () => {
clearTimeout ( resizeTimer );
resizeTimer = setTimeout (() => {
ScrollTrigger . refresh ();
}, 250 );
});
6. Lazy Load Animations
Defer animations for off-screen content:
window . addEventListener ( 'page-ready' , () => {
// Only animate elements currently in viewport
ScrollTrigger . batch ( '.lazy-animate' , {
once: true , // Only animate once
onEnter : ( elements ) => {
gsap . to ( elements , { opacity: 1 , y: 0 });
}
});
});
Debugging Animations
Visualize trigger points during development:
gsap . to ( '.element' , {
scrollTrigger: {
trigger: '.element' ,
start: 'top 80%' ,
markers: true , // Show visual markers
},
opacity: 1
});
Markers show:
Green : start position
Red : end position
Purple : Element position
Remove markers: true in production.
// Get all ScrollTriggers
console . log ( ScrollTrigger . getAll ());
// Disable all ScrollTriggers
ScrollTrigger . getAll (). forEach ( st => st . disable ());
// Enable all ScrollTriggers
ScrollTrigger . getAll (). forEach ( st => st . enable ());
// Kill specific ScrollTrigger
const st = ScrollTrigger . getById ( 'my-trigger-id' );
st ?. kill ();
// Monitor GSAP ticker performance
gsap . ticker . add (() => {
console . log ( 'FPS:' , gsap . ticker . fps );
});
// Log ScrollTrigger calculations
ScrollTrigger . addEventListener ( 'refresh' , () => {
console . log ( 'ScrollTrigger refreshed' );
});
Common Animation Patterns
Staggered Grid Animation
< div class = "grid" >
< div class = "grid-item" > 1 </ div >
< div class = "grid-item" > 2 </ div >
< div class = "grid-item" > 3 </ div >
< div class = "grid-item" > 4 </ div >
</ div >
< script >
import gsap from 'gsap' ;
import { ScrollTrigger } from 'gsap/ScrollTrigger' ;
gsap . registerPlugin ( ScrollTrigger );
window . addEventListener ( 'page-ready' , () => {
gsap . from ( '.grid-item' , {
opacity: 0 ,
y: 50 ,
stagger: 0.1 , // 0.1s between each item
duration: 0.8 ,
scrollTrigger: {
trigger: '.grid' ,
start: 'top 75%' ,
}
});
});
</ script >
< div class = "progress-bar" ></ div >
< style >
.progress-bar {
position : fixed ;
top : 0 ;
left : 0 ;
height : 4 px ;
background : #1A1A1A ;
width : 0 ;
z-index : 9999 ;
}
</ style >
< script >
import gsap from 'gsap' ;
import { ScrollTrigger } from 'gsap/ScrollTrigger' ;
gsap . registerPlugin ( ScrollTrigger );
window . addEventListener ( 'page-ready' , () => {
gsap . to ( '.progress-bar' , {
width: '100%' ,
ease: 'none' ,
scrollTrigger: {
trigger: 'body' ,
start: 'top top' ,
end: 'bottom bottom' ,
scrub: 0.3 ,
}
});
});
</ script >
Pinned Section
< section class = "pinned-section" >
< h2 > This section stays pinned </ h2 >
</ section >
< section class = "spacer" style = "height: 200vh;" >
<!-- Content below -->
</ section >
< script >
import gsap from 'gsap' ;
import { ScrollTrigger } from 'gsap/ScrollTrigger' ;
gsap . registerPlugin ( ScrollTrigger );
window . addEventListener ( 'page-ready' , () => {
ScrollTrigger . create ({
trigger: '.pinned-section' ,
start: 'top top' ,
end: '+=100%' ,
pin: true ,
pinSpacing: true ,
});
});
</ script >
Next Steps
Local Development Set up your development environment
Adding Components Create animated components
GSAP Documentation Official GSAP documentation
ScrollTrigger Demos Interactive ScrollTrigger examples