Docs
Go back
Component by
OV
OblivionTSX
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>
)
}"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-revealDependencies
npm install lucide-reactUsage Example
tsx
import ScrollReveal from "@/components/oblivion/scroll-reveal"
export default function Demo() {
return (
<div className="p-10 flex justify-center">
<ScrollReveal />
</div>
)
}import ScrollReveal from "@/components/oblivion/scroll-reveal"
export default function Demo() {
return (
<div className="p-10 flex justify-center">
<ScrollReveal />
</div>
)
}