Menu

Cinematic Text Reveal

Exploded 3D characters animate out of chaos into perfect alignment as the user scrolls, framed by a soft lighting background.

Preview

Live interactive preview.

Installation

Add this component to your project using the CLI:

terminal
npx -y vui-registry-cli-v1@latest add cinematic-text-reveal

Source Code

cinematic-text-reveal.tsx
'use client';

import React, { useEffect, useRef } from 'react';
import gsap from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';

gsap.registerPlugin(ScrollTrigger);

export default function CinematicTextReveal() {
  const containerRef = useRef<HTMLDivElement>(null);
  const textRef = useRef<HTMLHeadingElement>(null);
  const bgRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    if (!containerRef.current || !textRef.current || !bgRef.current) return;
    
    // Simple manual split text for the exact string to avoid relying on GSAP SplitText premium plugin
    const characters = textRef.current.querySelectorAll('.char');
    
    const ctx = gsap.context(() => {
      
      const tl = gsap.timeline({
        scrollTrigger: {
          trigger: containerRef.current,
          start: "top top",
          end: "+=150%",
          scrub: 1, // Smooth scrub
          pin: true,
        }
      });
      
      // 1. Initial State: Chaos (scattered characters)
      gsap.set(characters, {
        opacity: 0,
        z: () => gsap.utils.random(-1000, 1000),
        x: () => gsap.utils.random(-500, 500),
        y: () => gsap.utils.random(-500, 500),
        rotateX: () => gsap.utils.random(-90, 90),
        rotateY: () => gsap.utils.random(-90, 90),
        rotateZ: () => gsap.utils.random(-90, 90),
      });

      // 2. Animate to perfect alignment while expanding the background
      tl.to(bgRef.current, {
        scale: 5,
        opacity: 1,
        duration: 1,
        ease: "power2.inOut"
      }, 0);
      
      tl.to(characters, {
        opacity: 1,
        z: 0,
        x: 0,
        y: 0,
        rotateX: 0,
        rotateY: 0,
        rotateZ: 0,
        stagger: {
          amount: 1,
          from: "random"
        },
        duration: 1.5,
        ease: "expo.out"
      }, 0);

      // 3. Fade out everything at the very end
      tl.to(characters, {
        opacity: 0,
        y: -50,
        stagger: 0.02,
        duration: 0.5,
      }, "+=0.5");
      
    }, containerRef);

    return () => ctx.revert();
  }, []);

  const text = "AESTHETICS";

  return (
    <div ref={containerRef} className="h-screen w-full bg-[#111111] overflow-hidden flex items-center justify-center perspective-[1000px] relative">
      
      {/* Expanding Soft Light Background */}
      <div 
        ref={bgRef} 
        className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[30vh] h-[30vh] bg-[#f4f4f5] rounded-full blur-[100px] opacity-0 mix-blend-screen pointer-events-none" 
      />
      
      <h1 
        ref={textRef} 
        className="text-[12vw] md:text-[15vw] font-serif tracking-tighter text-[#F4F4F5] uppercase flex relative z-10 mix-blend-difference"
      >
        {text.split('').map((char, i) => (
          <span key={i} className="char inline-block will-change-transform drop-shadow-[0_0_30px_rgba(255,255,255,0.2)]">
            {char}
          </span>
        ))}
      </h1>
      
      <div className="absolute bottom-12 text-[#F4F4F5]/30 text-xs font-mono uppercase tracking-[0.3em]">
        Scroll to align
      </div>
    </div>
  );
}

Props

Component property reference.

NameTypeDefaultDescription
classNamestring''Additional CSS classes.
Context Worth Keeping In Orbit

Most components here are inspired by outstanding libraries and creators in the ecosystem. I don’t claim to be the original author — this is my space for learning, rebuilding, and understanding great work at a deeper level.

I’m still a student of the craft, constantly studying the best and translating what I learn through my own perspective. Every piece reflects curiosity, respect for the community, and small creative touches that feel true to me.

I’ve done my best to credit inspirations properly. If anything is missing or inaccurate, I truly appreciate a message so it can be corrected with care.