# Split text

> Stagger anything by chars, words or lines.

- Type: Component (reveal)
- Page: https://textlab.dev/components/split-text
- Markdown: https://textlab.dev/components/split-text.md
- Registry item: https://textlab.dev/r/split-text.json
- Source file: `registry/textlab/split-text/split-text.tsx`
- Dependencies: motion
- Variants: words, chars, lines
- Keywords: split, stagger, chars, words, lines, splittext

## When to use

- Custom entrances the other reveals do not cover.
- Choreography where order matters: center out, edges in.

## When not to use

- When a named reveal already fits; they carry better defaults.
- Long body copy split by chars.

## Installation

### CLI

```bash
npx shadcn@latest add @textlab/split-text
pnpm dlx shadcn@latest add @textlab/split-text
```

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/split-text.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 { SplitText } from "@/components/textlab/split-text/split-text";

export function Example() {
  return <SplitText variant="chars" order="center" preset="zoom" text="From the middle out" />;
}
```

## API reference

### SplitText

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `text` (required) | `string` | – | The copy. |
| `variant` | `"chars" \| "words" \| "lines"` | `"words"` | How the text is split. Lines are measured from layout and re-measured on resize. |
| `preset` | `"fade-up" \| "fade-down" \| "zoom" \| "blur" \| "flip"` | `"fade-up"` | A named starting pose. |
| `from` | `TargetAndTransition \| (index, total) => TargetAndTransition` | – | Custom starting pose per part. Overrides preset. |
| `order` | `"start" \| "end" \| "center" \| "edges"` | `"start"` | Which part moves first. |
| `clip` | `boolean` | `false` | Clip each part so it appears out of nothing. |
| `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` | – | Seconds between parts. |
| `duration` | `number` | `0.8` | 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

- One visually hidden copy for screen readers; every part is aria-hidden.
- Splits by grapheme, so emoji and combined letters stay whole.

## Motion

- Default stagger: 20 ms per char, 60 ms per word, 100 ms per line.
- Opacity leads, filter follows, transform settles last.

## Reduced motion

Shows plain text with no splitting.

## Performance

- Up to ~200 parts per instance.
- Line mode measures once per frame on resize.

## 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: words, chars, lines.

## Related

- [Character reveal](https://textlab.dev/components/char-reveal.md): Letters arrive one at a time.
- [Line reveal](https://textlab.dev/components/line-reveal.md): Lines lift in, re-measured on resize.
- [Blur reveal](https://textlab.dev/components/blur-reveal.md): Words sharpen into focus.

## Also in reveal

- [Word reveal](https://textlab.dev/components/word-reveal.md): Words rise out of a clip.
- [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.
- [Tracking reveal](https://textlab.dev/components/tracking-reveal.md): Letter spacing breathes open as it appears.

## Source

### components/textlab/split-text/split-text.tsx

```tsx
"use client";

import type { TargetAndTransition } from "motion/react";
import { RevealText, type RevealBaseProps, type RevealOrder } from "../lib/reveal";
import type { SplitBy } from "../lib/split";

export type SplitTextVariant = "chars" | "words" | "lines";
export type SplitTextPreset = "fade-up" | "fade-down" | "zoom" | "blur" | "flip";

export interface SplitTextProps extends RevealBaseProps {
  /** How the text is split. lines are measured from layout and re-measured on resize. */
  variant?: SplitTextVariant;
  /** A named starting pose. Ignored when `from` is set. */
  preset?: SplitTextPreset;
  /** Full control over where each part starts. Receives the part index and count when a function. */
  from?: TargetAndTransition | ((index: number, total: number) => TargetAndTransition);
  /** Which part moves first. */
  order?: RevealOrder;
  /** Clip each part so it appears out of nothing. */
  clip?: boolean;
}

const PRESETS: Record<SplitTextPreset, TargetAndTransition> = {
  "fade-up": { opacity: 0, y: "0.4em" },
  "fade-down": { opacity: 0, y: "-0.4em" },
  zoom: { opacity: 0, scale: 0.6 },
  blur: { opacity: 0, filter: "blur(8px)" },
  flip: { opacity: 0, transformPerspective: 500, rotateX: 90 },
};

/**
 * Stagger anything by chars, words or lines. The escape hatch under every reveal: pick a split,
 * a starting pose and an order, and it handles screen readers, line detection and reduced motion.
 */
export function SplitText({ variant = "words", preset = "fade-up", from, order = "start", clip = false, ...props }: SplitTextProps) {
  const by: SplitBy = variant;
  return (
    <RevealText
      by={by}
      order={order}
      clip={clip}
      hidden={from ?? PRESETS[preset]}
      partStyle={preset === "flip" && !from ? { transformOrigin: "50% 100%" } : undefined}
      {...props}
    />
  );
}

export default SplitText;
```

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