# Slice reveal

> The title arrives in offset horizontal slices.

- Type: Component (reveal)
- Page: https://textlab.dev/components/slice-reveal
- Markdown: https://textlab.dev/components/slice-reveal.md
- Registry item: https://textlab.dev/r/slice-reveal.json
- Source file: `registry/textlab/slice-reveal/slice-reveal.tsx`
- Dependencies: motion
- Variants: 5, 3, 8
- Keywords: slice, stripes, offset, print, registration, poster

## When to use

- Bold single-line display type, posters and title cards.
- Brands with a print or editorial feel.

## When not to use

- Text that wraps onto several lines; slices are cut across the whole block.
- Small sizes where slices are thinner than a stroke.

## Installation

### CLI

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

export function Example() {
  return <SliceReveal as="h2" variant="8" text="Registered" />;
}
```

## API reference

### SliceReveal

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `text` (required) | `string` | – | The copy. |
| `variant` | `"3" \| "5" \| "8"` | `"5"` | Number of horizontal slices. |
| `offset` | `number` | `0.35` | Starting distance of each slice as a share of the width. |
| `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.05` | Seconds between parts. |
| `duration` | `number` | `0.8` | Seconds each part takes. |
| `ease` | `EaseName \| [number, number, number, number]` | `"emphasized"` | 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 slices are aria-hidden copies; screen readers read the text once.
- An invisible copy reserves the exact size, so nothing shifts.

## Motion

- Slices alternate direction and start 50 ms apart.
- Opacity resolves early so the shape reads before the slices align.

## Reduced motion

Shows the line whole and still.

## Responsive behavior

- Keeps the line on one row; size it with a fluid font size.

## Performance

- N copies of the text, each with a static clip-path and a transform.

## 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: 5, 3, 8.

## Related

- [Glitch text](https://textlab.dev/components/glitch-text.md): A controlled digital glitch on cue.
- [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.

## Also in reveal

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

## Source

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

```tsx
"use client";

import type { CSSProperties, ElementType } from "react";
import { motion } from "motion/react";
import { SrText } from "../lib/a11y";
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 SliceRevealVariant = "3" | "5" | "8";

export interface SliceRevealProps {
  text: string;
  /** Number of horizontal slices. */
  variant?: SliceRevealVariant;
  /** How far each slice starts from home, as a share of the text width. */
  offset?: number;
  as?: ElementType;
  trigger?: Exclude<Trigger, "always">;
  active?: boolean;
  once?: boolean;
  delay?: number;
  stagger?: number;
  duration?: number;
  ease?: EaseInput;
  reducedMotion?: ReducedMotion;
  className?: string;
  style?: CSSProperties;
  onComplete?: () => void;
}

/**
 * The title arrives in offset horizontal slices that slide together, alternating sides,
 * like a print registered by hand. Each slice is a clipped copy of the same line.
 */
export function SliceReveal({
  text,
  variant = "5",
  offset = 0.35,
  as: Tag = "span",
  trigger = "inView",
  active,
  once = true,
  delay = 0,
  stagger = 0.05,
  duration = durations.reveal,
  ease = "emphasized",
  reducedMotion = "user",
  className,
  style,
  onComplete,
}: SliceRevealProps) {
  const { ref, on, bind } = useTrigger<HTMLElement>({ trigger, active, once });
  const hydrated = useHydrated();
  const reduced = useReduced(reducedMotion);
  const count = Number(variant);

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

  const curve = resolveEase(ease);
  return (
    <Tag
      ref={ref}
      className={className}
      style={{ display: "inline-block", ...style }}
      data-tl-motion={reducedMotion === "user" ? "user" : undefined}
      data-tl-pending={hydrated ? undefined : ""}
      {...bind}
    >
      <SrText>{text}</SrText>
      <span aria-hidden="true" style={{ position: "relative", display: "inline-block" }}>
        {/* The invisible copy holds the size, so slices can be absolutely positioned on top. */}
        <span style={{ visibility: "hidden" }}>{text}</span>
        {Array.from({ length: count }, (_, i) => {
          const top = (i / count) * 100;
          const bottom = 100 - ((i + 1) / count) * 100;
          const from = (i % 2 === 0 ? -1 : 1) * offset * 100;
          return (
            <motion.span
              key={i}
              data-tl-part=""
              style={{
                position: "absolute",
                inset: 0,
                whiteSpace: "nowrap",
                // A hair of overlap hides seams between slices at fractional pixel heights.
                clipPath: `inset(${Math.max(0, top - 0.6)}% -10% ${Math.max(0, bottom - 0.6)}% -10%)`,
              }}
              initial={{ transform: `translateX(${from}%)`, opacity: 0 }}
              animate={on ? { transform: "translateX(0%)", opacity: 1 } : { transform: `translateX(${from}%)`, opacity: 0 }}
              transition={
                on
                  ? { transform: { duration, delay: delay + i * stagger, ease: curve }, opacity: { duration: duration * 0.4, delay: delay + i * stagger } }
                  : { duration: durations.fast }
              }
              onAnimationComplete={on && i === count - 1 ? onComplete : undefined}
            >
              {text}
            </motion.span>
          );
        })}
      </span>
    </Tag>
  );
}

export default SliceReveal;
```

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