Skip to main content
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:
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

package.json
{
  "dependencies": {
    "gsap": "^3.14.2",
    "lenis": "^1.3.16"
  }
}

Lenis Smooth Scrolling

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:
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

BaseLayout.astro
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:
textRevealAnimation.ts
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:
textSwipeAnimation.ts
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

global.css
.text-reveal-wrapper {
  overflow: hidden;
  display: inline-block;
  width: 100%;
}

.text-reveal {
  opacity: 0;
  transform: translateY(60px);
  will-change: transform, opacity;
}

CSS Keyframe Animations

Pre-built CSS animations in animations.css:
animations.css
@keyframes fadeIn {
  from {
    opacity: 0;
    transform: translateY(30px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

@keyframes slideInLeft {
  from {
    opacity: 0;
    transform: translateX(-50px);
  }
  to {
    opacity: 1;
    transform: translateX(0);
  }
}

@keyframes slideInRight {
  from {
    opacity: 0;
    transform: translateX(50px);
  }
  to {
    opacity: 1;
    transform: translateX(0);
  }
}

@keyframes scaleIn {
  from {
    opacity: 0;
    transform: scale(0.9);
  }
  to {
    opacity: 1;
    transform: scale(1);
  }
}

Animation Utility Classes

animations.css
.animate-fade-in {
  animation: fadeIn 0.8s ease-out forwards;
}

.animate-slide-left {
  animation: slideInLeft 0.8s ease-out forwards;
}

.animate-slide-right {
  animation: slideInRight 0.8s ease-out forwards;
}

.animate-scale {
  animation: scaleIn 0.6s ease-out forwards;
}

Delay Classes

animations.css
.delay-100 { animation-delay: 0.1s; }
.delay-200 { animation-delay: 0.2s; }
.delay-300 { animation-delay: 0.3s; }
.delay-400 { animation-delay: 0.4s; }
.delay-500 { animation-delay: 0.5s; }

ScrollTrigger Animations

The main animation system provides several pre-built scroll-triggered effects:

Fade-In Sections

animations.ts
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

animations.ts
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

animations.ts
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

animations.ts
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

animations.css
.hover-lift {
  transition: transform var(--transition-smooth), 
              box-shadow var(--transition-smooth);
}

.hover-lift:hover {
  transform: translateY(-8px);
  box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15);
}

.hover-brightness {
  transition: filter var(--transition-fast);
}

.hover-brightness:hover {
  filter: brightness(1.1);
}

Image Zoom on Hover

animations.ts
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:
BaseLayout.astro
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:
animations.ts
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
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

Build docs developers (and LLMs) love