Pre-Loader

A cinematic GSAP-powered loading screen with typographic reveal and percentage counter.

Overview

A polished full-screen loading sequence that orchestrates a typographic reveal with a live percentage counter. Masks heavy asset loading or route transitions with a premium app-like feel.


Features

  • Choreographed rotation and scale animations
  • Live 0-100% counter with easing
  • GSAP timeline-based sequencing
  • Callback system for completion events
  • Embedded mode for previews

Preview

Live Preview
Z
0%

💡 Reload the page to see the animation again

The pre-loader plays once on page load


Installation

Install via the ZenBlocks CLI to automatically handle dependencies:

npx shadcn@latest add https://zenblocks-three.vercel.app/r/pre-loader.json

Method 2: Manual

  1. Install Dependencies

    npm install gsap @gsap/react
  2. Copy the Source Code

    Copy the code below into components/zenblocks/pre-loader.tsx.

    Click to expand source
    "use client";
    
    import React, { useRef } from "react";
    import { useGSAP } from "@gsap/react";
    import gsap from "gsap";
    import { cn } from "@/lib/utils";
    
    /* -------------------------------------------------------------------------- */
    /*                                   TYPES                                    */
    /* -------------------------------------------------------------------------- */
    
    
    export type PreLoaderProps = {
      onComplete?: () => void;
      /**
       * The text to display during the loading animation.
       * @default "S"
       */
      text?: string;
      /**
       * When true, renders inside a container instead of fullscreen.
       * Used for docs / previews.
       */
      embedded?: boolean;
      className?: string;
    };
    
    /* -------------------------------------------------------------------------- */
    /*                                PRELOADER                                   */
    /* -------------------------------------------------------------------------- */
    
    const PreLoader: React.FC<PreLoaderProps> = ({
      onComplete,
      text = "Z",
      embedded = false,
      className,
    }) => {
      const screenRef = useRef<HTMLDivElement | null>(null);
      const textRef = useRef<HTMLDivElement | null>(null);
      const percentRef = useRef<HTMLDivElement | null>(null);
    
      useGSAP(
        () => {
          if (!screenRef.current || !textRef.current || !percentRef.current) return;
    
          /* ---------------------------- Initial State ---------------------------- */
    
          gsap.set(textRef.current, {
            scale: 0,
            rotation: 180,
          });
    
          gsap.set(percentRef.current, {
            opacity: 0,
            y: 20,
          });
    
          /* ------------------------------ Animate In ----------------------------- */
    
          gsap.to(textRef.current, {
            scale: 1,
            rotation: 0,
            duration: 1,
            ease: "back.out(1.7)",
          });
    
          gsap.to(percentRef.current, {
            opacity: 1,
            y: 0,
            duration: 0.5,
            delay: 0.5,
            ease: "power2.out",
          });
    
          /* ------------------------- Percentage Counter -------------------------- */
    
          gsap.to(percentRef.current, {
            innerHTML: 100,
            duration: 2.5,
            delay: 1,
            ease: "power2.inOut",
            snap: { innerHTML: 1 },
            onUpdate() {
              const el = this.targets()[0] as HTMLElement;
              const value = Math.round(Number(el.innerHTML));
              el.innerHTML = `${value}%`;
            },
          });
    
          /* ------------------------------ Pulse Hold ----------------------------- */
    
          gsap.to(textRef.current, {
            scale: 1.1,
            duration: 0.5,
            delay: 1,
            yoyo: true,
            repeat: 1,
            ease: "power2.inOut",
          });
    
          /* ------------------------------ Animate Out ----------------------------- */
    
          gsap.to(textRef.current, {
            scale: 0,
            rotation: -180,
            duration: 0.8,
            delay: 2.5,
            ease: "back.in(1.7)",
          });
    
          /* ---------------------------- Screen Fade ------------------------------ */
    
          gsap.to(screenRef.current, {
            opacity: 0,
            duration: 0.5,
            delay: 3.3,
            ease: "power1.inOut",
            onComplete: () => {
              onComplete?.();
            },
          });
        },
        { dependencies: [onComplete] }
      );
    
      return (
        <div
          ref={screenRef}
          className={cn(
            "z-50 pointer-events-none flex items-center justify-center",
            embedded ? "absolute inset-0" : "fixed inset-0",
    
            /* Surface */
            "bg-white text-zinc-900",
            "dark:bg-black dark:text-white",
            className
          )}
        >
          {/* Main Letter */}
          <div
            ref={textRef}
            className={cn(
              "text-[100px] sm:text-[150px] md:text-[200px]",
              "font-black tracking-tight",
              "text-current" // Force inheritance
            )}
          >
            {text}
          </div>
    
          {/* Percentage counter */}
          <div
            ref={percentRef}
            className={cn(
              "absolute bottom-6 right-6",
              "text-4xl sm:text-5xl md:text-6xl",
              "font-light italic",
              "opacity-60" // Use opacity instead of explicit color
            )}
          >
            0%
          </div>
        </div>
      );
    };
    
    export default PreLoader;

Usage

import { useState } from "react";
import PreLoader from "@/components/zenblocks/pre-loader";

export default function Layout({ children }) {
  const [loading, setLoading] = useState(true);

  return (
    <>
      {loading && <PreLoader onComplete={() => setLoading(false)} />}
      {!loading && children}
    </>
  );
}

Props

PropTypeDefaultDescription
textstring"Z"Central character/logo to display
onComplete() => void-Callback when animation finishes
embeddedbooleanfalseRender absolute instead of fixed
classNamestring-Additional CSS classes

Accessibility

  • Temporarily holds visual focus during load
  • Counter updates hidden from screen readers (aria-live="off")
  • Respects prefers-reduced-motion for transitions
  • High contrast text in both themes

Customization

Custom Text

<PreLoader text="ZEN" />

Colors

<PreLoader className="bg-blue-600 text-white" />

Motion Behavior

Phase 1: Text scales up from 0 and rotates to 0deg with back.out(1.7) easing
Phase 2: Counter runs from 0% to 100% over 2.5 seconds
Phase 3: Text exits with reverse rotation, screen fades out

Total sequence duration: ~3.8 seconds


Performance Notes

  • Uses single GSAP timeline for efficient cleanup
  • Removes itself from DOM after completion (via parent state)
  • Counter uses snap for integer values
  • Zero long-term DOM weight after exit

Examples

Route Transition

// In Next.js layout.tsx
<PreLoader key={pathname} onComplete={handleComplete} />

Notes

  • Best placed at root of React tree (layout.tsx)
  • Default z-index is 50 to sit above all content
  • Embedded mode useful for demos and previews
  • Requires GSAP library