The Adosa Real Estate website uses GSAP 3.14.2 for animations and Lenis 1.3.16 for smooth scrolling, creating a premium, polished user experience.
GSAP Configuration
GSAP is configured globally in src/scripts/animations.ts:
import { gsap } from 'gsap' ;
import { ScrollTrigger } from 'gsap/ScrollTrigger' ;
// Register plugins
gsap . registerPlugin ( ScrollTrigger );
// Global GSAP configuration
gsap . defaults ({
ease: 'power3.out' ,
duration: 1 ,
});
Dependencies
{
"dependencies" : {
"gsap" : "^3.14.2" ,
"lenis" : "^1.3.16"
}
}
Lenis provides buttery-smooth scroll behavior, but is only enabled on desktop (screens > 1024px) to avoid conflicts with native touch scrolling on mobile devices.
Configuration
Lenis is initialized in BaseLayout.astro:
import Lenis from "lenis" ;
// Only initialize on desktop (>1024px)
const isDesktop = window . matchMedia ( "(min-width: 1025px)" ). matches ;
if ( isDesktop ) {
const lenis = new Lenis ({
duration: 0 , // Zero inertia (instant stop)
easing : ( t ) => t , // Linear easing
orientation: "vertical" ,
gestureOrientation: "vertical" ,
smoothWheel: true ,
wheelMultiplier: 0.5 , // Reduced speed for premium feel
});
// Expose globally for other scripts
window . lenis = lenis ;
function raf ( time : number ) {
lenis . raf ( time );
requestAnimationFrame ( raf );
}
requestAnimationFrame ( raf );
}
Mobile Behavior: Lenis is disabled on mobile/tablet to preserve native touch scrolling. Always test scroll interactions on real mobile devices.
Lenis CSS Styles
html .lenis {
height : auto ;
}
.lenis.lenis-smooth {
scroll-behavior : auto !important ;
}
.lenis.lenis-stopped {
overflow : hidden ;
}
.lenis.lenis-scrolling iframe {
pointer-events : none ;
}
Text Reveal Animations
Two utility functions provide elegant text reveal effects:
textRevealAnimation.ts
Basic text reveal with scroll trigger:
import { gsap } from "gsap" ;
import { ScrollTrigger } from "gsap/ScrollTrigger" ;
export interface TextRevealOptions {
selector ?: string ;
stagger ?: number ;
start ?: string ;
duration ?: number ;
}
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 ,
opacity: 1 ,
duration ,
ease: "power3.out" ,
scrollTrigger: {
trigger: element ,
start ,
toggleActions: "play none none reverse" ,
},
delay: index * stagger ,
});
});
}
textSwipeAnimation.ts
Advanced swipe-up animation with custom delays:
export function createTextSwipeAnimation (
selector : string ,
sectionTrigger : Element
) : ScrollTrigger | undefined {
const elements = document . querySelectorAll ( selector );
if ( elements . length === 0 ) return undefined ;
const tl = gsap . timeline ({
scrollTrigger: {
trigger: sectionTrigger ,
start: "top 50%" ,
toggleActions: "play none none reverse" ,
}
});
elements . forEach (( element , index ) => {
const el = element as HTMLElement ;
const customDelay = el . dataset . delay
? parseFloat ( el . dataset . delay )
: null ;
const startTime = customDelay !== null
? customDelay
: index * 0.15 ;
tl . to ( element , {
y: 0 ,
opacity: 1 ,
duration: 1.2 ,
ease: "power3.out"
}, startTime );
});
return tl . scrollTrigger ;
}
HTML Structure for Text Reveals
< div class = "text-reveal-wrapper" >
< h1 class = "text-reveal" > Luxury Real Estate </ h1 >
</ div >
< div class = "text-reveal-wrapper" >
< p class = "text-reveal" data-delay = "0.3" > Premium properties </ p >
</ div >
CSS Classes
.text-reveal-wrapper {
overflow : hidden ;
display : inline-block ;
width : 100 % ;
}
.text-reveal {
opacity : 0 ;
transform : translateY ( 60 px );
will-change : transform, opacity;
}
CSS Keyframe Animations
Pre-built CSS animations in animations.css:
@keyframes fadeIn {
from {
opacity : 0 ;
transform : translateY ( 30 px );
}
to {
opacity : 1 ;
transform : translateY ( 0 );
}
}
@keyframes slideInLeft {
from {
opacity : 0 ;
transform : translateX ( -50 px );
}
to {
opacity : 1 ;
transform : translateX ( 0 );
}
}
@keyframes slideInRight {
from {
opacity : 0 ;
transform : translateX ( 50 px );
}
to {
opacity : 1 ;
transform : translateX ( 0 );
}
}
@keyframes scaleIn {
from {
opacity : 0 ;
transform : scale ( 0.9 );
}
to {
opacity : 1 ;
transform : scale ( 1 );
}
}
Animation Utility Classes
.animate-fade-in {
animation : fadeIn 0.8 s ease-out forwards ;
}
.animate-slide-left {
animation : slideInLeft 0.8 s ease-out forwards ;
}
.animate-slide-right {
animation : slideInRight 0.8 s ease-out forwards ;
}
.animate-scale {
animation : scaleIn 0.6 s ease-out forwards ;
}
Delay Classes
.delay-100 { animation-delay : 0.1 s ; }
.delay-200 { animation-delay : 0.2 s ; }
.delay-300 { animation-delay : 0.3 s ; }
.delay-400 { animation-delay : 0.4 s ; }
.delay-500 { animation-delay : 0.5 s ; }
The main animation system provides several pre-built scroll-triggered effects:
Fade-In Sections
export function initScrollAnimations () {
const fadeElements = document . querySelectorAll ( '.fade-in-section' );
fadeElements . forEach (( element ) => {
gsap . to ( element , {
opacity: 1 ,
y: 0 ,
duration: 1.2 ,
ease: 'power3.out' ,
scrollTrigger: {
trigger: element ,
start: 'top 85%' ,
end: 'bottom 20%' ,
toggleActions: 'play none none reverse' ,
},
});
});
}
Stagger Groups
const staggerGroups = document . querySelectorAll ( '.stagger-group' );
staggerGroups . forEach (( group ) => {
const children = group . children ;
gsap . fromTo ( children ,
{ opacity: 0 , y: 40 },
{
opacity: 1 ,
y: 0 ,
duration: 0.8 ,
stagger: 0.15 ,
ease: 'power3.out' ,
scrollTrigger: {
trigger: group ,
start: 'top 85%' ,
toggleActions: 'play none none reverse' ,
},
}
);
});
Parallax Effects
export function initParallaxEffects () {
const parallaxBgs = document . querySelectorAll ( '.parallax-bg' );
parallaxBgs . forEach (( element ) => {
const speed = parseFloat (
element . getAttribute ( 'data-speed' ) || '0.5'
);
gsap . to ( element , {
y : () => element . offsetHeight * speed ,
ease: 'none' ,
scrollTrigger: {
trigger: element . parentElement ,
start: 'top bottom' ,
end: 'bottom top' ,
scrub: 0 ,
},
});
});
}
Counter Animations
export function initCounterAnimations () {
const counters = document . querySelectorAll ( '.counter' );
counters . forEach (( counter ) => {
const targetText = counter . getAttribute ( 'data-target' ) || '0' ;
const target = parseInt ( targetText . match ( / \d + / )?.[ 0 ] || '0' );
ScrollTrigger . create ({
trigger: counter ,
start: 'top 80%' ,
once: true ,
onEnter : () => {
gsap . to ( counter , {
innerHTML: target ,
duration: 2.5 ,
snap: { innerHTML: 1 },
ease: 'power2.out' ,
});
},
});
});
}
Hover Effects
CSS Hover Utilities
.hover-lift {
transition : transform var ( --transition-smooth ),
box-shadow var ( --transition-smooth );
}
.hover-lift:hover {
transform : translateY ( -8 px );
box-shadow : 0 12 px 40 px rgba ( 0 , 0 , 0 , 0.15 );
}
.hover-brightness {
transition : filter var ( --transition-fast );
}
.hover-brightness:hover {
filter : brightness ( 1.1 );
}
Image Zoom on Hover
export function initImageHoverEffects () {
const images = document . querySelectorAll ( '.zoom-on-hover img' );
images . forEach (( img ) => {
const parent = img . parentElement ;
parent ?. addEventListener ( 'mouseenter' , () => {
gsap . to ( img , {
scale: 1.1 ,
duration: 0.8 ,
ease: 'power2.out' ,
});
});
parent ?. addEventListener ( 'mouseleave' , () => {
gsap . to ( img , {
scale: 1 ,
duration: 0.8 ,
ease: 'power2.out' ,
});
});
});
}
Page Transitions
White fade transition between pages:
function handlePageTransition ( e : MouseEvent ) {
const target = e . target . closest ( "a" );
if ( ! target || target . target === "_blank" ) return ;
e . preventDefault ();
// Create white overlay
let overlay = document . getElementById ( "transition-overlay" );
if ( ! overlay ) {
overlay = document . createElement ( "div" );
overlay . id = "transition-overlay" ;
overlay . style . cssText = `
position: fixed;
top: 0; left: 0;
width: 100vw; height: 100vh;
background-color: #FFFFFF;
z-index: 99999;
opacity: 0;
transition: opacity 0.6s cubic-bezier(0.25, 1, 0.5, 1);
` ;
document . body . appendChild ( overlay );
}
// Fade in
requestAnimationFrame (() => {
overlay . style . opacity = "1" ;
});
// Navigate after transition
setTimeout (() => {
window . location . href = target . getAttribute ( "href" );
}, 600 );
}
document . addEventListener ( "click" , handlePageTransition );
Usage Examples
Basic Fade-In Section
< section class = "fade-in-section" >
< h2 > Our Services </ h2 >
< p > Premium real estate solutions </ p >
</ section >
Stagger Animation Group
< div class = "stagger-group grid grid-3" >
< div class = "card" > Property 1 </ div >
< div class = "card" > Property 2 </ div >
< div class = "card" > Property 3 </ div >
</ div >
Text Reveal with Custom Delay
< div class = "text-reveal-wrapper" >
< h1 class = "text-reveal" data-delay = "0.5" >
Luxury Living
</ h1 >
</ div >
Parallax Background
< section class = "hero" >
< div class = "parallax-bg" data-speed = "0.5" >
< img src = "/hero.jpg" alt = "Hero" />
</ div >
</ section >
Counter Animation
< div class = "counter" data-target = "20+" >
0+
</ div >
Initialization
Initialize all animations at once:
export function initAllAnimations () {
initScrollAnimations ();
initParallaxEffects ();
initCounterAnimations ();
initImageHoverEffects ();
// Refresh ScrollTrigger after setup
setTimeout (() => {
ScrollTrigger . refresh ();
}, 100 );
}
// Auto-initialize
initAllAnimations ();
Best Practices
Lenis is disabled on mobile—always test scroll animations on real devices, not just browser dev tools
Use will-change sparingly
Only apply will-change: transform to elements that are actively animating to avoid performance issues
Call ScrollTrigger.refresh() after images load or layout changes to recalculate trigger positions
Too many simultaneous animations can feel chaotic. Use stagger and delays to create rhythm
Consider respecting prefers-reduced-motion for accessibility
Typography Text reveal animation setup
Responsive Design Mobile animation considerations