# Wipe reveal

> A soft edge uncovers the line.

- Type: Component (reveal)
- Page: https://textlab.dev/components/wipe-reveal
- Markdown: https://textlab.dev/components/wipe-reveal.md
- Registry item: https://textlab.dev/r/wipe-reveal.json
- Source file: `registry/textlab/wipe-reveal/wipe-reveal.tsx`
- Dependencies: motion
- Variants: left, right, center, diagonal
- Keywords: wipe, mask, sweep, unveil, gradient

## When to use

- Titles that should feel unveiled rather than assembled.
- Text that must stay selectable and unsplit.

## When not to use

- Multi-line paragraphs; the sweep crosses the whole block at once.
- When you need per-word timing; use word-reveal.

## Installation

### CLI

```bash
npx shadcn@latest add @textlab/wipe-reveal
pnpm dlx shadcn@latest add @textlab/wipe-reveal
```

The `@textlab` name needs `"registries": { "@textlab": "https://textlab.dev/r/{name}.json" }` in `components.json`. Without it, use the full URL:

```bash
npx shadcn@latest add https://textlab.dev/r/wipe-reveal.json
```

Import the tokens once in your root layout: `import "@/components/textlab/tokens.css";`

### Manual

1. Install the dependencies: `npm install motion@12`
2. Copy `text-fx` (https://textlab.dev/r/text-fx.json) and this component into `components/textlab/`.

## Usage

```tsx
import { WipeReveal } from "@/components/textlab/wipe-reveal/wipe-reveal";

export function Example() {
  return <WipeReveal as="h2" variant="center" text="Unveil the headline" />;
}
```

## API reference

### WipeReveal

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `text` (required) | `string` | – | The copy. |
| `variant` | `"left" \| "right" \| "center" \| "diagonal"` | `"left"` | Direction of the sweep; center opens from the middle. |
| `softness` | `number` | `0.18` | Width of the feathered edge as a share of the text, 0 to 0.5. |
| `as` | `ElementType` | `"span"` | Rendered element. |
| `trigger` | `"mount" \| "inView" \| "hover" \| "manual"` | `"inView"` | What starts the animation. manual follows `active`. |
| `active` | `boolean` | `false` | Drives trigger="manual". |
| `once` | `boolean` | `true` | In view: play once instead of on every entry. |
| `delay` | `number` | `0` | Seconds before the first part moves. |
| `duration` | `number` | `1` | Seconds each part takes. |
| `ease` | `EaseName \| [number, number, number, number]` | `"standard"` | Easing token or cubic bezier. |
| `reducedMotion` | `"user" \| "always" \| "never"` | `"user"` | user follows the OS setting; always shows the still version. |
| `className` | `string` | – | Merged onto the root. |
| `onComplete` | `() => void` | – | Called when a one-shot animation finishes. |

## Accessibility

- The text is never split, so assistive tech reads it as plain text.
- Mask padding keeps descenders inside without changing layout.

## Motion

- The edge travels with the standard ease so the sweep speed feels even.
- It overshoots by the soft width so the last letter finishes fully opaque.

## Reduced motion

Shows the text with no mask.

## Performance

- Animates one CSS variable on one element; the mask is GPU composited.

## Notes for AI

- Use only the props documented above. Do not invent props.
- Keep the screen-reader text and the reduced-motion branch when editing the source.
- Pick a variant from: left, right, center, diagonal.

## Related

- [In-view title](https://textlab.dev/components/in-view-title.md): Bring a section title in as it scrolls into view.
- [Line reveal](https://textlab.dev/components/line-reveal.md): Lines lift in, re-measured on resize.
- [Outline draw](https://textlab.dev/components/outline-draw.md): The outline draws itself, then fills.

## Also in reveal

- [Blur reveal](https://textlab.dev/components/blur-reveal.md): Words sharpen into focus.
- [Word reveal](https://textlab.dev/components/word-reveal.md): Words rise out of a clip.
- [Character reveal](https://textlab.dev/components/char-reveal.md): Letters arrive one at a time.
- [Split text](https://textlab.dev/components/split-text.md): Stagger anything by chars, words or lines.

## Source

### components/textlab/wipe-reveal/wipe-reveal.tsx

```tsx
"use client";

import type { CSSProperties, ElementType } from "react";
import { motion } from "motion/react";
import { duration as durations, resolveEase, type EaseInput } from "../lib/motion-tokens";
import { useHydrated } from "../lib/use-hydrated";
import { useReduced, type ReducedMotion } from "../lib/use-reduced";
import { useTrigger, type Trigger } from "../lib/use-trigger";

export type WipeRevealVariant = "left" | "right" | "center" | "diagonal";

export interface WipeRevealProps {
  text: string;
  /** Direction of the sweep. center opens from the middle outward. */
  variant?: WipeRevealVariant;
  /** Width of the soft edge as a share of the text, 0 to 0.5. */
  softness?: number;
  as?: ElementType;
  trigger?: Exclude<Trigger, "always">;
  active?: boolean;
  once?: boolean;
  amount?: number;
  delay?: number;
  duration?: number;
  ease?: EaseInput;
  reducedMotion?: ReducedMotion;
  className?: string;
  style?: CSSProperties;
  id?: string;
  onComplete?: () => void;
}

function mask(variant: WipeRevealVariant, edge: number): string {
  const e = `${edge * 100}%`;
  if (variant === "center") {
    return `linear-gradient(90deg, transparent calc(50% - var(--tl-wipe)), #000 calc(50% - var(--tl-wipe) + ${e}), #000 calc(50% + var(--tl-wipe) - ${e}), transparent calc(50% + var(--tl-wipe)))`;
  }
  const angle = variant === "left" ? 90 : variant === "right" ? 270 : 115;
  return `linear-gradient(${angle}deg, #000 calc(var(--tl-wipe) - ${e}), transparent var(--tl-wipe))`;
}

/**
 * A soft edge uncovers the line. The mask sweeps a CSS variable, so no letter is ever cut by a hard
 * clip and the text stays real, selectable text the whole time.
 */
export function WipeReveal({
  text,
  variant = "left",
  softness = 0.18,
  as: Tag = "span",
  trigger = "inView",
  active,
  once = true,
  amount,
  delay = 0,
  duration = durations.reveal * 1.25,
  ease = "standard",
  reducedMotion = "user",
  className,
  style,
  id,
  onComplete,
}: WipeRevealProps) {
  const { ref, on, bind } = useTrigger<HTMLElement>({ trigger, active, once, amount });
  const hydrated = useHydrated();
  const reduced = useReduced(reducedMotion);
  const edge = Math.min(0.5, Math.max(0, softness));
  // The sweep runs past the far edge by the soft width, so the last letter ends fully opaque.
  const end = variant === "center" ? `${50 + edge * 100}%` : `${100 + edge * 100 + (variant === "diagonal" ? 20 : 0)}%`;
  const image = mask(variant, edge);

  if (reduced) {
    return (
      <Tag ref={ref} id={id} className={className} style={style} {...bind}>
        {text}
      </Tag>
    );
  }

  return (
    <Tag
      ref={ref}
      id={id}
      className={className}
      style={style}
      data-tl-motion={reducedMotion === "user" ? "user" : undefined}
      data-tl-pending={hydrated ? undefined : ""}
      {...bind}
    >
      <motion.span
        data-tl-part=""
        style={{
          display: "inline-block",
          padding: "0.1em 0.12em 0.22em",
          margin: "-0.1em -0.12em -0.22em",
          WebkitMaskImage: image,
          maskImage: image,
        }}
        initial={{ "--tl-wipe": "0%", opacity: 0.001 } as never}
        animate={(on ? { "--tl-wipe": end, opacity: 1 } : { "--tl-wipe": "0%", opacity: 0.001 }) as never}
        transition={on ? { duration, delay, ease: resolveEase(ease), opacity: { duration: 0.01, delay } } : { duration: durations.fast }}
        onAnimationComplete={on ? onComplete : undefined}
      >
        {text}
      </motion.span>
    </Tag>
  );
}

export default WipeReveal;
```

Full library index: https://textlab.dev/llms.txt
