# Scroll Reveal

Wrap anything to fade it in as it scrolls into view, from any direction or out of a blur. Several children stagger.

**Category:** Display  
**Exports:** `ScrollReveal`  
**npm dependencies:** motion (+ clsx, tailwind-merge for `cn`)

## Install

```bash
npx shadcn@latest add https://charcoalui.com/r/scroll-reveal.json
```
The file lands at `components/ui/scroll-reveal.tsx`; import from `@/components/ui/scroll-reveal`.

## API — Scroll Reveal
| Prop | Type | Default | Notes |
| --- | --- | --- | --- |
| `from` | `"up" \| "down" \| "left" \| "right" \| "scale" \| "blur"` | `"up"` |  |
| `stagger` | `number` | `0.08` |  |
| `delay` | `number` | `0` |  |
| `once` | `boolean` | `true` |  |
| `amount` | `number` | `0.25` |  |

## Example — Scroll Reveal

```tsx
import * as React from "react";
import { ScrollReveal } from "@/components/ui/scroll-reveal";

export default function ScrollRevealDemo() {
  return (
    <div className="h-80 w-full max-w-md overflow-y-auto rounded-xl border bg-background p-6">
      <p className="text-sm text-muted-foreground">Scroll down ↓</p>
      <div className="h-56" />
      <ScrollReveal className="grid gap-3" from="up">
        {["Springs", "Tokens", "Ember", "Reduced motion"].map((t) => (
          <div key={t} className="rounded-xl border bg-card p-4 font-medium">
            {t}
          </div>
        ))}
      </ScrollReveal>
      <div className="h-10" />
      <ScrollReveal from="blur">
        <p className="text-center text-2xl font-medium tracking-tight">Revealed with blur.</p>
      </ScrollReveal>
      <div className="h-16" />
    </div>
  );
}
```

## Source — components/ui/scroll-reveal.tsx

```tsx
"use client";

import * as React from "react";
import { motion, useReducedMotion, type Variants } from "motion/react";

export type ScrollRevealProps = {
  children: React.ReactNode;
  /** How the content enters. */
  from?: "up" | "down" | "left" | "right" | "scale" | "blur";
  /** Seconds between children, if you pass several. */
  stagger?: number;
  delay?: number;
  once?: boolean;
  /** Fraction of the element visible before it reveals. */
  amount?: number;
  className?: string;
};

const offsets: Record<NonNullable<ScrollRevealProps["from"]>, object> = {
  up: { y: 24 },
  down: { y: -24 },
  left: { x: -32 },
  right: { x: 32 },
  scale: { scale: 0.92 },
  blur: { filter: "blur(10px)" },
};

/** Fade content in as it scrolls into view. Wrap several children to stagger them. */
export function ScrollReveal({ children, from = "up", stagger = 0.08, delay = 0, once = true, amount = 0.25, className }: ScrollRevealProps) {
  const reduce = useReducedMotion();
  const item: Variants = {
    hidden: { opacity: 0, ...(reduce ? {} : offsets[from]) },
    shown: { opacity: 1, x: 0, y: 0, scale: 1, filter: "blur(0px)", transition: { type: "spring", stiffness: 220, damping: 26 } },
  };
  return (
    <motion.div
      className={className}
      initial="hidden"
      whileInView="shown"
      viewport={{ once, amount }}
      transition={{ staggerChildren: reduce ? 0 : stagger, delayChildren: delay }}
    >
      {React.Children.map(children, (child) => (
        <motion.div variants={item}>{child}</motion.div>
      ))}
    </motion.div>
  );
}
```