Oblivion
Oblivion
Docs
Go back
Component by
OV
Oblivion
TSX
scroll-reveal
"use client"

import { useEffect, useRef, useState, ReactNode } from "react"
import { cn } from "@/lib/utils"
import { Zap, ShieldCheck } from "lucide-react"

export const componentMeta = {
  name: "Scroll Reveal",
  description: "Wraps any content and animates it into view when it enters the viewport.",
  props: {
    children:  { type: "ReactNode",                                   description: "Content to reveal" },
    direction: { type: "'up' | 'down' | 'left' | 'right' | 'scale'", default: '"up"', description: "Direction of entry animation" },
    delay:     { type: "number",  default: "0",                       description: "Delay in ms before animation starts" },
    duration:  { type: "number",  default: "600",                     description: "Animation duration in ms" },
    once:      { type: "boolean", default: "true",                    description: "Animate only the first time it enters view" },
    threshold: { type: "number",  default: "0.15",                    description: "Fraction of element visible before triggering" },
  }
}

type Direction = "up" | "down" | "left" | "right" | "scale"

const HIDDEN: Record<Direction, string> = {
  up:    "opacity-0 translate-y-8",
  down:  "opacity-0 -translate-y-8",
  left:  "opacity-0 translate-x-8",
  right: "opacity-0 -translate-x-8",
  scale: "opacity-0 scale-90",
}
const VISIBLE = "opacity-100 translate-y-0 translate-x-0 scale-100"

export default function ScrollReveal({
  children,
  direction = "up",
  delay     = 0,
  duration  = 600,
  once      = true,
  threshold = 0.15,
  className,
}: {
  children:   ReactNode
  direction?: Direction
  delay?:     number
  duration?:  number
  once?:      boolean
  threshold?: number
  className?: string
}) {
  const ref      = useRef<HTMLDivElement>(null)
  const [shown, setShown] = useState(false)

  useEffect(() => {
    const el = ref.current
    if (!el) return
    const obs = new IntersectionObserver(
      ([entry]) => {
        if (entry.isIntersecting) {
          setShown(true)
          if (once) obs.disconnect()
        } else if (!once) {
          setShown(false)
        }
      },
      { threshold }
    )
    obs.observe(el)
    return () => obs.disconnect()
  }, [once, threshold])

  return (
    <div
      ref={ref}
      className={cn(
        "transition-all",
        shown ? VISIBLE : HIDDEN[direction],
        className
      )}
      style={{
        transitionDuration: `${duration}ms`,
        transitionDelay:    `${delay}ms`,
        transitionTimingFunction: "cubic-bezier(0.22, 1, 0.36, 1)",
      }}
    >
      {children}
    </div>
  )
}

export function ScrollRevealPreview() {
  return (
    <div className="flex h-[450px] w-full flex-col items-center bg-transparent overflow-y-auto no-scrollbar py-12 px-6 px-6">
      
      {/* ── Section 1: Heroish Reveal ── */}
      <div className="mb-32 text-center max-w-lg">
        <ScrollReveal direction="up" delay={0} once={false}>
          <p className="text-xs font-bold uppercase tracking-[0.3em] text-indigo-500 mb-3">The Future of UI</p>
        </ScrollReveal>
        <ScrollReveal direction="up" delay={150} once={false}>
          <h2 className="text-4xl md:text-5xl font-black tracking-tight text-zinc-900 dark:text-white leading-[1.1]">
            Experience Motion <br /> <span className="text-transparent bg-clip-text bg-gradient-to-r from-indigo-500 to-purple-500">Like Never Before.</span>
          </h2>
        </ScrollReveal>
        <ScrollReveal direction="up" delay={300} once={false}>
          <p className="mt-6 text-zinc-500 dark:text-zinc-400 text-sm leading-relaxed">
            Scroll down to see elements elegantly reveal themselves with our physics-inspired scroll reveal engine. 
          </p>
        </ScrollReveal>
      </div>

      {/* ── Section 2: Feature Grid ── */}
      <div className="grid grid-cols-1 md:grid-cols-2 gap-6 w-full max-w-2xl mb-32 px-4">
        <ScrollReveal direction="left" duration={800} once={false}>
          <div className="group p-8 rounded-3xl border border-zinc-100 dark:border-zinc-800 bg-white dark:bg-zinc-900/50 hover:border-zinc-200 dark:hover:border-zinc-700 transition-all shadow-sm">
            <div className="w-12 h-12 rounded-2xl bg-indigo-50 dark:bg-indigo-900/30 flex items-center justify-center mb-6">
              <Zap className="w-6 h-6 text-indigo-500" strokeWidth={2.5} />
            </div>
            <h3 className="text-xl font-bold text-zinc-900 dark:text-white mb-3">Ultra-Fast</h3>
            <p className="text-sm text-zinc-500 dark:text-zinc-400 leading-relaxed font-medium">Optimized with IntersectionObserver for buttery smooth performance.</p>
          </div>
        </ScrollReveal>

        <ScrollReveal direction="right" duration={800} once={false}>
          <div className="group p-8 rounded-3xl border border-zinc-100 dark:border-zinc-800 bg-white dark:bg-zinc-900/50 hover:border-zinc-200 dark:hover:border-zinc-700 transition-all shadow-sm">
            <div className="w-12 h-12 rounded-2xl bg-rose-50 dark:bg-rose-900/30 flex items-center justify-center mb-6">
               <ShieldCheck className="w-6 h-6 text-rose-500" strokeWidth={2.5} />
            </div>
            <h3 className="text-xl font-bold text-zinc-900 dark:text-white mb-3">Secure API</h3>
            <p className="text-sm text-zinc-500 dark:text-zinc-400 leading-relaxed font-medium">Built with TypeScript first, ensuring rock-solid stability for your app.</p>
          </div>
        </ScrollReveal>
      </div>

      {/* ── Section 3: Value Prop ── */}
      <div className="mb-32 text-center w-full max-w-xl">
        <div className="grid grid-cols-2 md:grid-cols-4 gap-6 px-4">
          {[1,2,3,4].map((i) => (
             <ScrollReveal key={i} direction="scale" delay={i * 100} once={false}>
                <div className="aspect-square rounded-[2rem] bg-zinc-50 dark:bg-zinc-900 border border-zinc-100 dark:border-zinc-800 flex items-center justify-center shadow-inner group transition-transform hover:scale-105 duration-500 pointer-events-auto">
                  <span className="text-4xl font-black text-zinc-200 dark:text-zinc-800 group-hover:text-indigo-500 transition-colors duration-500 cursor-default">{i}</span>
                </div>
             </ScrollReveal>
          ))}
        </div>
        <ScrollReveal key="bottom-text" direction="up" delay={200} once={false} className="mt-12">
           <p className="text-xs font-semibold text-zinc-500 dark:text-zinc-400 uppercase tracking-widest animate-pulse">Everything scales perfectly</p>
        </ScrollReveal>
      </div>

      {/* ── Section 4: Final CTA ── */}
      <div className="text-center pb-20">
        <ScrollReveal direction="down" once={false}>
          <div className="inline-flex items-center gap-3 px-8 py-4 rounded-full bg-zinc-900 dark:bg-white text-white dark:text-black font-extrabold text-sm shadow-2xl shadow-indigo-500/20 cursor-pointer hover:scale-110 active:scale-95 transition-all">
            Get Started with Oblivion →
          </div>
        </ScrollReveal>
      </div>

    </div>
  )
}
Save to favoritesCopy to Figma
React

Scroll Reveal

Wraps any content and animates it into view when it enters the viewport.

Installation

$ npx @cosmoo/oblivion add scroll-reveal
Dependenciesnpm install lucide-react

Usage Example

tsx
import ScrollReveal from "@/components/oblivion/scroll-reveal"

export default function Demo() {
  return (
    <div className="p-10 flex justify-center">
      <ScrollReveal />
    </div>
  )
}