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 motion

Add 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:

components/ui/scroll-reveal.tsx
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

PropTypeRequiredDefaultDescription
childrenReactNodeYes—Content to reveal.
variant"fade-up" | "blur" | "scale" | "slide-left" | "slide-right" | "clip"No"fade-up"Reveal style.
rootRefObject<Element>No—Scroll container to watch instead of the window.
amountnumberNo0.25Share of the element that must be visible, 0–1.
oncebooleanNotrueReveal once, or hide again when it leaves view.
delay / durationnumberNo0 / 0.6Seconds. Delay is capped at 0.6 so nothing waits too long.
classNamestringNo—Classes for the wrapper.

Best Practices

  1. Use it on editorial and marketing sections; never hide content people need right away, like navigation or form fields.
  2. Pick one or two styles per page. Alternating slide-left and slide-right gives a list rhythm.
  3. Inside a scrollable panel, pass the panel as root, otherwise the reveal fires against the window.
  4. For several items arriving together, give each a small delay (0.05–0.1 s apart).