# Character reveal

> Letters arrive one at a time.

- Type: Component (reveal)
- Page: https://textlab.dev/components/char-reveal
- Markdown: https://textlab.dev/components/char-reveal.md
- Registry item: https://textlab.dev/r/char-reveal.json
- Source file: `registry/textlab/char-reveal/char-reveal.tsx`
- Dependencies: motion
- Variants: fade, drop, spin, scale
- Keywords: character, letter, stagger, drop, spin, pop

## When to use

- Short display words and names.
- Moments where the arrival itself is the point.

## When not to use

- Anything over ~60 letters; the tail gets long.
- Text people need to read quickly.

## Installation

### CLI

```bash
npx shadcn@latest add @textlab/char-reveal
pnpm dlx shadcn@latest add @textlab/char-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/char-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 { CharReveal } from "@/components/textlab/char-reveal/char-reveal";

export function Example() {
  return <CharReveal variant="drop" text="Hello there" />;
}
```

## API reference

### CharReveal

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `text` (required) | `string` | – | The copy. |
| `variant` | `"fade" \| "drop" \| "spin" \| "scale"` | `"fade"` | fade is quiet, drop falls into place, spin turns up from the baseline, scale pops up from small. |
| `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. |
| `stagger` | `number` | `0.02` | Seconds between parts. |
| `duration` | `number` | `0.6 (fade), 0.7` | Seconds each part takes. |
| `ease` | `EaseName \| [number, number, number, number]` | `"enter"` | 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

- Letters are aria-hidden; the word is read once.
- Grapheme-aware, so emoji and accents never split.

## Motion

- 20 ms between letters; the total stays under a second for most words.
- Words are nowrap, so lines never jump while letters move.

## Reduced motion

Shows the text immediately.

## Performance

- One motion span per letter. Fine for headings, not paragraphs.

## 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: fade, drop, spin, scale.

## Related

- [Blur reveal](https://textlab.dev/components/blur-reveal.md): Words sharpen into focus.
- [Split text](https://textlab.dev/components/split-text.md): Stagger anything by chars, words or lines.
- [Focus pull](https://textlab.dev/components/focus-pull.md): Focus racks from the center letter outward.

## Also in reveal

- [Word reveal](https://textlab.dev/components/word-reveal.md): Words rise out of a clip.
- [Line reveal](https://textlab.dev/components/line-reveal.md): Lines lift in, re-measured on resize.
- [Wipe reveal](https://textlab.dev/components/wipe-reveal.md): A soft edge uncovers the line.
- [In-view title](https://textlab.dev/components/in-view-title.md): Bring a section title in as it scrolls into view.

## Source

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

```tsx
"use client";

import { RevealText, type RevealBaseProps } from "../lib/reveal";

export type CharRevealVariant = "fade" | "drop" | "spin" | "scale";

export interface CharRevealProps extends RevealBaseProps {
  /** fade is quiet, drop falls into place, spin turns up from the baseline, scale pops from nothing. */
  variant?: CharRevealVariant;
}

const HIDDEN = {
  fade: { opacity: 0, y: "0.12em" },
  drop: { opacity: 0, y: "-0.7em" },
  spin: { opacity: 0, rotate: -90, scale: 0.5 },
  scale: { opacity: 0, scale: 0.35 },
} as const;

/** Letters arrive one at a time. Each word stays unbreakable so lines never reflow mid-animation. */
export function CharReveal({ variant = "fade", duration, ...props }: CharRevealProps) {
  return (
    <RevealText
      by="chars"
      hidden={HIDDEN[variant]}
      duration={duration ?? (variant === "fade" ? 0.6 : 0.7)}
      partStyle={variant === "spin" ? { transformOrigin: "0% 100%" } : variant === "scale" ? { transformOrigin: "50% 70%" } : undefined}
      {...props}
    />
  );
}

export default CharReveal;
```

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