# Depth layers

> Outline copies drift apart in depth as you scroll.

- Type: Component (scroll)
- Page: https://textlab.dev/components/depth-layers
- Markdown: https://textlab.dev/components/depth-layers.md
- Registry item: https://textlab.dev/r/depth-layers.json
- Source file: `registry/textlab/depth-layers/depth-layers.tsx`
- Dependencies: motion
- Variants: stack, fan
- Keywords: parallax, depth, layers, outline, stack, scroll

## When to use

- Big, bold section titles on scroll-led pages.
- Posters and editorial covers.

## When not to use

- Thin or small type; outlines disappear.
- Tight layouts; layers extend below the text.

## Installation

### CLI

```bash
npx shadcn@latest add @textlab/depth-layers
pnpm dlx shadcn@latest add @textlab/depth-layers
```

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/depth-layers.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 { DepthLayers } from "@/components/textlab/depth-layers/depth-layers";

export function Example() {
  return <DepthLayers as="h2" text="Parallax" layers={6} />;
}
```

## API reference

### DepthLayers

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `text` (required) | `string` | – | The copy. |
| `variant` | `"stack" \| "fan"` | `"stack"` | Separate vertically like layered prints, or fan open around a corner. |
| `layers` | `number` | `5` | Outline copies behind the solid one. |
| `spread` | `number` | `0.12em (stack), 4° (fan)` | Largest gap between neighbouring copies. |
| `container` | `RefObject<HTMLElement>` | – | Scrollable ancestor. Defaults to the window. |
| `as` | `ElementType` | `"span"` | Rendered element. |
| `reducedMotion` | `"user" \| "always" \| "never"` | `"user"` | user follows the OS setting; always shows the still version. |
| `className` | `string` | – | Merged onto the root. |

## Accessibility

- Copies are aria-hidden; the title is read once.
- The solid copy never moves, so the title stays readable.

## Motion

- Widest at the middle of the viewport, closed at the edges.
- A spring smooths scroll jitter.

## Reduced motion

Only the solid title, no layers.

## Performance

- N transformed copies; transform and opacity only.

## 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: stack, fan.

## Related

- [Extrude text](https://textlab.dev/components/extrude-text.md): Depth grows from the letters on hover.
- [Scroll skew](https://textlab.dev/components/scroll-skew.md): Type leans with the speed of your scroll.
- [Tilt text](https://textlab.dev/components/tilt-text.md): Extruded type tilts toward the pointer.

## Also in scroll

- [Scroll fill](https://textlab.dev/components/scroll-fill.md): Words light up as you read down the page.
- [Velocity marquee](https://textlab.dev/components/velocity-marquee.md): A marquee that speeds up, slows and reverses with scroll.
- [Scroll stretch](https://textlab.dev/components/scroll-stretch.md): Letters stretch tall as you scroll fast.

## Source

### components/textlab/depth-layers/depth-layers.tsx

```tsx
"use client";

import { useRef, type CSSProperties, type ElementType, type RefObject } from "react";
import { motion, useScroll, useSpring, useTransform, type MotionValue } from "motion/react";
import { srOnly } from "../lib/a11y";
import { useReduced, type ReducedMotion } from "../lib/use-reduced";

export type DepthLayersVariant = "stack" | "fan";

export interface DepthLayersProps {
  text: string;
  /** stack separates copies vertically like layered prints; fan swings them open around a corner. */
  variant?: DepthLayersVariant;
  /** Number of outline copies behind the solid one. */
  layers?: number;
  /** Largest gap between neighbouring copies, in em (stack) or degrees (fan). */
  spread?: number;
  container?: RefObject<HTMLElement | null>;
  as?: ElementType;
  reducedMotion?: ReducedMotion;
  className?: string;
  style?: CSSProperties;
}

function Layer({ text, index, layers, progress, variant, spread }: { text: string; index: number; layers: number; progress: MotionValue<number>; variant: DepthLayersVariant; spread: number }) {
  const depth = index + 1;
  const y = useTransform(progress, (p) => (variant === "stack" ? `${p * depth * spread}em` : "0em"));
  const rotate = useTransform(progress, (p) => (variant === "fan" ? p * depth * spread : 0));
  return (
    <motion.span
      aria-hidden="true"
      style={{
        position: "absolute",
        inset: 0,
        y,
        rotate,
        transformOrigin: "0% 100%",
        WebkitTextFillColor: "transparent",
        WebkitTextStroke: "0.015em currentColor",
        opacity: 1 - depth / (layers + 1.5),
        zIndex: -depth,
      }}
    >
      {text}
    </motion.span>
  );
}

/**
 * Stacked outline copies drift apart in depth as the title moves through the viewport, widest at
 * the center of the screen and closing again as it leaves.
 */
export function DepthLayers({ text, variant = "stack", layers = 5, spread, container, as: Tag = "span", reducedMotion = "user", className, style }: DepthLayersProps) {
  const ref = useRef<HTMLElement | null>(null);
  const reduced = useReduced(reducedMotion);
  const { scrollYProgress } = useScroll({ target: ref, container, offset: ["start end", "end start"] });
  // 0 at the edges of the viewport, 1 at its center.
  const open = useTransform(scrollYProgress, [0, 0.5, 1], [0, 1, 0]);
  const progress = useSpring(open, { stiffness: 180, damping: 30 });
  const gap = spread ?? (variant === "stack" ? 0.12 : 4);

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

  return (
    <Tag ref={ref} className={className} style={{ position: "relative", display: "inline-block", isolation: "isolate", ...style }}>
      <span style={srOnly}>{text}</span>
      {Array.from({ length: layers }, (_, i) => (
        <Layer key={i} text={text} index={i} layers={layers} progress={progress} variant={variant} spread={gap} />
      ))}
      <span aria-hidden="true" style={{ position: "relative" }}>
        {text}
      </span>
    </Tag>
  );
}

export default DepthLayers;
```

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