VeloxUI
scroll·free

Scroll Reveal

Fade-and-slide elements into view on scroll, with optional stagger and blur.

Fast

Scroll into view to reveal each card in sequence.

Composable

Scroll into view to reveal each card in sequence.

Typed

Scroll into view to reveal each card in sequence.

Installation

terminal
pnpm add gsap @gsap/react

Then copy the component below into src/components/motion/scroll-reveal.tsx. It expects @/lib/gsap (plugin registration) and @/lib/utils (cn).

Source

src/components/motion/scroll-reveal.tsx
"use client";

import { useRef, type ReactNode } from "react";
import { gsap, useGSAP } from "@/lib/gsap";
import { cn } from "@/lib/utils";

type Direction = "up" | "down" | "left" | "right" | "none";

export interface ScrollRevealProps {
  children: ReactNode;
  className?: string;
  /** Direction the element travels from. */
  from?: Direction;
  /** Travel distance in px. */
  distance?: number;
  duration?: number;
  delay?: number;
  /** Stagger direct children instead of animating the wrapper. */
  stagger?: number;
  /** ScrollTrigger start position, e.g. "top 80%". */
  start?: string;
  once?: boolean;
  blur?: boolean;
}

const offset = (from: Direction, d: number) => {
  switch (from) {
    case "up":
      return { y: d };
    case "down":
      return { y: -d };
    case "left":
      return { x: d };
    case "right":
      return { x: -d };
    default:
      return {};
  }
};

export function ScrollReveal({
  children,
  className,
  from = "up",
  distance = 40,
  duration = 0.9,
  delay = 0,
  stagger = 0,
  start = "top 85%",
  once = true,
  blur = false,
}: ScrollRevealProps) {
  const ref = useRef<HTMLDivElement>(null);

  useGSAP(
    () => {
      const el = ref.current;
      if (!el) return;
      const targets = stagger > 0 ? Array.from(el.children) : el;
      gsap.fromTo(
        targets,
        { opacity: 0, filter: blur ? "blur(12px)" : "none", ...offset(from, distance) },
        {
          opacity: 1,
          x: 0,
          y: 0,
          filter: "blur(0px)",
          duration,
          delay,
          stagger,
          ease: "power3.out",
          scrollTrigger: {
            trigger: el,
            start,
            toggleActions: once ? "play none none none" : "play none none reverse",
          },
        }
      );
    },
    { scope: ref, dependencies: [from, distance, duration, delay, stagger, start, once, blur] }
  );

  return (
    <div ref={ref} className={cn(className)}>
      {children}
    </div>
  );
}