← Library

Animation

Preloader Reveal Animation

Vanilla JSViteGSAP

A full-screen preloader with a circular SVG progress indicator and animated text lines, that wipes off screen on click to reveal a scaled hero section. Uses GSAP SplitText, CustomEase, and clip-path polygon sweeps.

Build a single-page preloader reveal animation using Vanilla JS, Vite, and GSAP. Use the following specifications exactly:

  ## Color & Typography
  - Light (--base-100): #ffffff
  - Gray (--base-200): #7a7a7a
  - Dark (--base-300): #000000
  - Font 1: "Barlow Condensed" weight 800, text-transform: uppercase, font-size: clamp(5rem, 15vw, 15rem), line-height: 0.8 — for the hero h1
  - Font 2: "Geist Mono" weight 500, text-transform: uppercase, font-size: 0.75rem, line-height: 1 — for all p elements
  - Load both fonts via Google Fonts @import in CSS

  ## HTML Structure
  Three stacked layers, all position: fixed, 100vw x 100svh:

  ### Layer 1 — Backdrop (.preloader-backdrop)
  - z-index: 0, background: var(--base-100), color: var(--base-200)
  - display: flex, flex-direction: column, justify-content: space-between
  - Two rows (.pb-row, padding: 1.5rem, display: flex, justify-content: space-between):
    - Row 1: 5 .pb-col elements with cryptic/hacker-aesthetic text + one #pb-logo img (2.5rem x 2.5rem, padding: 0.25rem, border: 1px dashed
  var(--base-200))
    - Row 2: 6 .pb-col elements aligned flex-end, with cryptic status strings
  - Mobile (max-width: 1000px): hide .pb-col:nth-child(1), :nth-child(2), :nth-child(5) in row 1

  ### Layer 2 — Preloader (.preloader)
  - z-index: 2, background: var(--base-300), color: var(--base-100)
  - display: flex, flex-direction: column, justify-content: space-between
  - clip-path: polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%) — full coverage
  - will-change: transform, clip-path
  - Two rows (.p-row, padding: 1.5rem, display: flex, justify-content: space-between):
    - Row 1: <p>Initiating</p>
    - Row 2: left .p-col (display: flex, gap: 6rem, align-items: flex-end) with two .p-sub-col (each with 2 <p> tags), right .p-col with <p>PX-17</p>
  - Centered absolute .preloader-btn-container: width: 20rem, height: 20rem, top: 50%, left: 50%, transform: translate(-50%, -50%)

  ### Central Button (.preloader-btn-container)
  All direct children: position: absolute, top: 50%, left: 50%, transform: translate(-50%, -50%)
  - #pbc-logo: white logo image, width: 4rem, height: 4rem
  - #pbc-label: <p>Engage</p>, font-size: 0.9rem
  - #pbc-outro-label: <p>Access Granted</p>, font-size: 0.9rem
  - .pbc-svg-strokes (width/height: 100%, will-change: transform):
    - SVG viewBox="0 0 320 320", two circles r=155, cx=160, cy=160:
      - .stroke-track: stroke="#2b2b2b", stroke-width="2"
      - .stroke-progress: stroke="#fff", stroke-width="2"

  ### Layer 3 — Hero (.hero)
  - position: relative, width: 100%, height: 100svh, padding: 1.5rem
  - background: var(--base-300), color: var(--base-100)
  - display: flex, justify-content: center, align-items: center, text-align: center
  - transform: scale(0.75), will-change: transform
  - Children:
    - .preloader-revealer: position: absolute, inset: 0, background: var(--base-100), clip-path: polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%), will-change:
   clip-path
    - <h1> (width: 90%): hero text content

  ## CSS Initial States
  ```css
  h1 .word,
  p .line {
    position: relative;
    transform: translateY(100%);              
    will-change: transform;                   
  }                                           
                                                                                                                                                          
  JavaScript                                                                                                                                              
                                                                                                                                                          
  Setup                                                                                                                                                   
                                                                                                                                                          
  import gsap from "gsap";                                                                                                                                
  import { SplitText } from "gsap/SplitText";                                                                                                             
  import CustomEase from "gsap/CustomEase";   
                                                                                                                                                          
  gsap.registerPlugin(SplitText, CustomEase);                                                                                                             
  CustomEase.create("hop", "0.9, 0, 0.1, 1");                                                                                                             
  CustomEase.create("glide", "0.8, 0, 0.2, 1");                                                                                                           
                                                                                                                                                          
  DOMContentLoaded                                                                                                                                        
                                                                                                                                                          
  document.addEventListener("DOMContentLoaded", () => {                                                                                                   
    let preloaderComplete = false;                                                                                                                        
                                                                                                                                                          
    const preloaderBtn = document.querySelector(".preloader-btn-container");                                                                              
    const btnOutlineTrack = document.querySelector(".stroke-track");                                                                                      
    const btnOutlineProgress = document.querySelector(".stroke-progress");                                                                                
    const svgPathLength = btnOutlineTrack.getTotalLength();                                                                                               
                                                                                                                                                          
    gsap.set([btnOutlineTrack, btnOutlineProgress], {                                                                                                     
      strokeDasharray: svgPathLength,                                                                                                                     
      strokeDashoffset: svgPathLength,                                                                                                                    
    });                                                                                                                                                   
                                                                                                                                                          
    document.querySelectorAll(".preloader p").forEach(p => {                                                                                              
      new SplitText(p, { type: "lines", linesClass: "line", mask: "lines" });                                                                             
    });                                                                                                                                                   
                                                                                                                                                          
    new SplitText(".hero h1", { type: "words", wordsClass: "word", mask: "words" });                                                                      
                                                                                                                                                          
  Entry Timeline (1s delay)                   
                                                                                                                                                          
    const introTl = gsap.timeline({ delay: 1 });
                                                                                                                                                          
    introTl                                   
      .to(".preloader .p-row p .line", {                                                                                                                  
        y: "0%", duration: 0.75, ease: "power3.out", stagger: 0.1,                                                                                        
      })                                                                                                                                                  
      .to(btnOutlineTrack, {                                                                                                                              
        strokeDashoffset: 0, duration: 2, ease: "hop",                                                                                                    
      }, "<")                                                                                                                                             
      .to(".pbc-svg-strokes svg", {                                                                                                                       
        rotation: 270, duration: 2, ease: "hop",                                                                                                          
      }, "<");                                                                                                                                            
                                                                                                                                                          
    const progressStops = [0.2, 0.25, 0.85, 1].map((base, i) => {                                                                                         
      if (i === 3) return 1;                                                                                                                              
      return base + (Math.random() - 0.5) * 0.1;                                                                                                          
    });                                                                                                                                                   
                                                                                                                                                          
    progressStops.forEach((stop, i) => {                                                                                                                  
      introTl.to(btnOutlineProgress, {                                                                                                                    
        strokeDashoffset: svgPathLength - svgPathLength * stop,                                                                                           
        duration: 0.75,                                                                                                                                   
        ease: "glide",                        
        delay: i === 0 ? 0.3 : 0.3 + Math.random() * 0.2,                                                                                                 
      });                                                                                                                                                 
    });                                                                                                                                                   
                                                                                                                                                          
    introTl                                                                                                                                               
      .to("#pbc-logo", { opacity: 0, duration: 0.35, ease: "power1.out" }, "-=0.25")                                                                      
      .to(preloaderBtn, { scale: 0.9, duration: 1.5, ease: "hop" }, "-=0.5")                                                                              
      .to("#pbc-label .line", {                                                                                                                           
        y: "0%", duration: 0.75, ease: "power3.out",                                                                                                      
        onComplete: () => { preloaderComplete = true; },                                                                                                  
      }, "-=0.75");                                                                                                                                       
                                                                                                                                                          
  Exit Timeline (click)                                                                                                                                   
                                                                                                                                                          
    preloaderBtn.addEventListener("click", () => {                                                                                                        
      if (!preloaderComplete) return;                                                                                                                     
      preloaderComplete = false;                                                                                                                          
                                                                                                                                                          
      const exitTl = gsap.timeline();         
                                                                                                                                                          
      exitTl                                                                                                                                              
        .to(".preloader", { scale: 0.75, duration: 1.25, ease: "hop" })                                                                                   
        .to([btnOutlineTrack, btnOutlineProgress], {                                                                                                      
          strokeDashoffset: -svgPathLength, duration: 1.25, ease: "hop",                                                                                  
        }, "<")                               
        .to("#pbc-label .line", { y: "-100%", duration: 0.75, ease: "power3.out" }, "-=1.25")                                                             
        .to("#pbc-outro-label .line", { y: "0%", duration: 0.75, ease: "power3.out" }, "-=0.75")                                                          
        .to(".preloader", {                                                                                                                               
          clipPath: "polygon(0% 0%, 0% 0%, 0% 100%, 0% 100%)",                                                                                            
          duration: 1.5, ease: "hop",                                                                                                                     
        })                                                                                                                                                
        .to(".preloader-revealer", {                                                                                                                      
          clipPath: "polygon(0% 0%, 0% 0%, 0% 100%, 0% 100%)",                                                                                            
          duration: 1.5, ease: "hop",                                                                                                                     
          onComplete: () => gsap.set(".preloader", { display: "none" }),                                                                                  
        }, "-=1.45")                                                                                                                                      
        .to(".hero", { scale: 1, duration: 1.25, ease: "hop" })                                                                                           
        .to(".hero h1 .word", {                                                                                                                           
          y: "0%", duration: 1, ease: "glide", stagger: 0.05,                                                                                             
        }, "-=1.75");                                                                                                                                     
    });                                                                                                                                                   
  });                                                                                                                                                     
                                                                                                                                                          
  File Structure                                                                                                                                          
                                                                                                                                                          
  /                                                                                                                                                       
    index.html                                                                                                                                            
    script.js                                                                                                                                             
    styles.css                                                                                                                                            
    package.json                              
    /public                                                                                                                                               
      logo.png           — dark version (backdrop row 1)                                                                                                  
      logo-light.png     — white version (preloader center #pbc-logo)                                                                                     
                                                                                                                                                          
  package.json                                                                                                                                            
                                                                                                                                                          
  {                                                                                                                                                       
    "scripts": { "dev": "vite" },                                                                                                                         
    "dependencies": {                                                                                                                                     
      "gsap": "^3.14.2",                                                                                                                                  
      "vite": "^8.0.0"                                                                                                                                    
    }                                                                                                                                                     
  }                                                                                                                                                       
                                                                                                                                                          
  Technical Notes                                                                                                                                         
                                                                                                                                                          
  - SplitText requires GSAP Club (Business Green or higher)                                                                                               
  - preloaderComplete boolean gates the click — prevents triggering exit before entry finishes                                                            
  - Wipe direction is left: both .preloader and .preloader-revealer animate from polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%) to polygon(0% 0%, 0% 0%, 0%
  100%, 0% 100%)                                                                                                                                          
  - .preloader-revealer is a white overlay INSIDE .hero — it masks the hero until the black preloader wipes off, then itself wipes off to reveal the dark
  hero beneath                                                                                                                                            
  - All .preloader p elements are SplitText-split, including #pbc-label and #pbc-outro-label                                                              
  - Hero starts at scale(0.75) and expands to 1 via the exit TL offset "-=1.75" (runs concurrently with the wipe)                                         
  - The SVG track draw and SVG rotation both start at "<" (same time as the lines stagger)