Overview
Content that arrives as it scrolls into view. Six styles: fade-up (a short upward drift), blur (sharpens into focus), scale (a slight grow), slide-left and slide-right, and clip (unrolls from the top). It can watch the page or a scrollable panel via root, reveal once or every time, and wait for a set share of the element to be visible. Reduced motion shows content immediately.
Installation
Install dependencies:
$ pnpm add motionAdd the utility function for class merging:
lib/utils.ts
tsx
import { ClassValue, clsx } from "clsx";import { twMerge } from "tailwind-merge"; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs));}
Copy the component code into your project:
tsx
"use client"; import { useRef, type ReactNode, type RefObject } from "react";import { motion, useInView, useReducedMotion, type TargetAndTransition } from "motion/react";import { cn } from "@/lib/utils"; export type ScrollRevealVariant = "fade-up" | "blur" | "scale" | "slide-left" | "slide-right" | "clip"; export type ScrollRevealProps = { children: ReactNode; className?: string; variant?: ScrollRevealVariant; /** Seconds to wait after entering view, capped at 0.6. */ delay?: number; /** Seconds for the reveal. */ duration?: number; /** Reveal only once (default) or whenever the element re-enters view. */ once?: boolean; /** How much of the element must be visible, 0–1. */ amount?: number; /** * Scroll container to watch instead of the window, for content inside a * scrollable panel. */ root?: RefObject<Element | null>;}; const EASE_OUT = [0.23, 1, 0.32, 1] as const; const HIDDEN: Record<ScrollRevealVariant, TargetAndTransition> = { "fade-up": { opacity: 0, y: 24 }, blur: { opacity: 0, y: 12, filter: "blur(10px)" }, scale: { opacity: 0, scale: 0.92, y: 12 }, "slide-left": { opacity: 0, x: -40 }, "slide-right": { opacity: 0, x: 40 }, clip: { opacity: 1, clipPath: "inset(0 0 100% 0 round 16px)", y: 16 },}; const SHOWN: Record<ScrollRevealVariant, TargetAndTransition> = { "fade-up": { opacity: 1, y: 0 }, blur: { opacity: 1, y: 0, filter: "blur(0px)" }, scale: { opacity: 1, scale: 1, y: 0 }, "slide-left": { opacity: 1, x: 0 }, "slide-right": { opacity: 1, x: 0 }, clip: { opacity: 1, clipPath: "inset(0 0 0% 0 round 16px)", y: 0 },}; export function ScrollReveal({ children, className, variant = "fade-up", delay = 0, duration = 0.6, once = true, amount = 0.25, root,}: ScrollRevealProps) { const ref = useRef<HTMLDivElement>(null); const reduce = useReducedMotion(); const inView = useInView(ref, { once, amount, root, margin: "0px 0px -8% 0px" }); if (reduce) return <div className={className}>{children}</div>; return ( <motion.div ref={ref} className={cn(className)} initial={HIDDEN[variant]} animate={inView ? SHOWN[variant] : HIDDEN[variant]} transition={{ duration: variant === "clip" ? duration * 1.2 : duration, delay: inView ? Math.max(0, Math.min(delay, 0.6)) : 0, ease: EASE_OUT, }} > {children} </motion.div> );}
Adjust import paths according to your folder structure.
Basic Usage
tsx
import { ScrollReveal } from "@/components/ui/scroll-reveal"; <ScrollReveal variant="blur"> <article>Content sharpens in as it reaches the viewport.</article></ScrollReveal> // Inside a scrollable panelconst panel = useRef<HTMLDivElement>(null); <div ref={panel} className="h-96 overflow-y-auto"> <ScrollReveal root={panel} variant="slide-left">…</ScrollReveal></div>
Props
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
| children | ReactNode | Yes | — | Content to reveal. |
| variant | "fade-up" | "blur" | "scale" | "slide-left" | "slide-right" | "clip" | No | "fade-up" | Reveal style. |
| root | RefObject<Element> | No | — | Scroll container to watch instead of the window. |
| amount | number | No | 0.25 | Share of the element that must be visible, 0–1. |
| once | boolean | No | true | Reveal once, or hide again when it leaves view. |
| delay / duration | number | No | 0 / 0.6 | Seconds. Delay is capped at 0.6 so nothing waits too long. |
| className | string | No | — | Classes for the wrapper. |
Best Practices
- Use it on editorial and marketing sections; never hide content people need right away, like navigation or form fields.
- Pick one or two styles per page. Alternating
slide-leftandslide-rightgives a list rhythm. - Inside a scrollable panel, pass the panel as
root, otherwise the reveal fires against the window. - For several items arriving together, give each a small
delay(0.05–0.1 s apart).