# Tilt text

> Extruded type tilts toward the pointer.

- Type: Component (cursor)
- Page: https://textlab.dev/components/tilt-text
- Markdown: https://textlab.dev/components/tilt-text.md
- Registry item: https://textlab.dev/r/tilt-text.json
- Source file: `registry/textlab/tilt-text/tilt-text.tsx`
- Dependencies: none
- Variants: extrude, float, shadow
- Keywords: 3d text, tilt, extrude, perspective, parallax, hover

## When to use

- Bold single words in hero sections.
- Product names and numbers with a physical feel.

## When not to use

- Long or wrapping text.
- Dense layouts; the tilt needs breathing room.

## Installation

### CLI

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

export function Example() {
  return <TiltText as="h1" text="Depth" />;
}
```

## API reference

### TiltText

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `text` (required) | `string` | – | The copy. |
| `variant` | `"extrude" \| "float" \| "shadow"` | `"extrude"` | Solid depth, floating above a ground shadow, or a long pointer-driven shadow. |
| `tilt` | `number` | `18` | Largest tilt in degrees. |
| `depth` | `number` | `12` | Extrusion layers. |
| `sideColor` | `string` | `a darker mix of the text color` | Color of the extruded sides. |
| `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

- Depth layers are aria-hidden copies; the word is read once.
- At rest the text faces forward and reads normally.

## Motion

- The tilt eases toward the pointer and back to center when it leaves.

## Reduced motion

Still, front-facing text with its depth intact.

## Performance

- Transforms on one 3D group; layers are static inside it.

## 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: extrude, float, shadow.

## Related

- [Extrude text](https://textlab.dev/components/extrude-text.md): Depth grows from the letters on hover.
- [Depth layers](https://textlab.dev/components/depth-layers.md): Outline copies drift apart in depth as you scroll.
- [Glass text](https://textlab.dev/components/glass-text.md): Glass letters that bend the scene behind them.

## Also in cursor

- [Magnetic text](https://textlab.dev/components/magnetic-text.md): Letters lean toward the cursor and spring back.
- [Variable proximity](https://textlab.dev/components/variable-proximity.md): Weight and width swell near the cursor.
- [Cursor trail](https://textlab.dev/components/cursor-trail.md): A word trails behind the cursor letter by letter.
- [Flip roll](https://textlab.dev/components/flip-roll.md): Hover and the label rolls up into a fresh copy.

## Source

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

```tsx
"use client";

import { useRef, type CSSProperties, type ElementType } from "react";
import { srOnly } from "../lib/a11y";
import { damp } from "../lib/math";
import { useFrame } from "../lib/use-frame";
import { usePointer } from "../lib/use-pointer";
import { useReduced, type ReducedMotion } from "../lib/use-reduced";

export type TiltTextVariant = "extrude" | "float" | "shadow";

export interface TiltTextProps {
  text: string;
  /** extrude gives the letters solid depth, float lifts them off a soft ground shadow, shadow casts a long light-driven shadow. */
  variant?: TiltTextVariant;
  /** Largest tilt in degrees. */
  tilt?: number;
  /** Extrusion layers for the extrude variant. */
  depth?: number;
  /** Side color of the extrusion. */
  sideColor?: string;
  as?: ElementType;
  reducedMotion?: ReducedMotion;
  className?: string;
  style?: CSSProperties;
}

// Rest slightly turned, so the depth reads even before anyone points at it.
const REST = { x: 0.7, y: 0.3 };

/**
 * Extruded type tilts toward the pointer. The extrusion is built from stacked copies pushed back in
 * real 3D space, so the sides turn with the face like a solid object.
 */
export function TiltText({ text, variant = "extrude", tilt = 18, depth = 12, sideColor = "color-mix(in oklab, currentColor 45%, var(--tl-background, #000))", as: Tag = "span", reducedMotion = "user", className, style }: TiltTextProps) {
  const root = useRef<HTMLElement | null>(null);
  const group = useRef<HTMLSpanElement | null>(null);
  const shadow = useRef<HTMLSpanElement | null>(null);
  const reduced = useReduced(reducedMotion);
  const pointer = usePointer(root, { enabled: !reduced });
  const pos = useRef({ ...REST });

  const rotate = (x: number, y: number) => `rotateX(${((y - 0.5) * -tilt).toFixed(2)}deg) rotateY(${((x - 0.5) * tilt * 1.4).toFixed(2)}deg)`;

  useFrame(
    (_, dt) => {
      const p = pointer.current;
      const tx = p.active && p.inside ? p.nx : REST.x;
      const ty = p.active && p.inside ? p.ny : REST.y;
      const before = pos.current;
      if (Math.abs(before.x - tx) < 0.0005 && Math.abs(before.y - ty) < 0.0005) return;
      pos.current = { x: damp(before.x, tx, 7, dt), y: damp(before.y, ty, 7, dt) };
      // Transforms are written to the elements themselves, never through inherited variables.
      if (group.current) group.current.style.transform = rotate(pos.current.x, pos.current.y);
      if (shadow.current) shadow.current.style.transform = `translate(${((0.5 - pos.current.x) * 0.4).toFixed(3)}em, ${((0.5 - pos.current.y) * 0.4 + 0.06).toFixed(3)}em) translateZ(-0.3em)`;
    },
    { ref: root, enabled: !reduced },
  );

  return (
    <Tag ref={root} className={className} style={{ position: "relative", display: "inline-block", perspective: "8em", padding: "0.2em 0.3em", ...style }}>
      <span style={srOnly}>{text}</span>
      <span ref={group} aria-hidden="true" style={{ position: "relative", display: "inline-block", transformStyle: "preserve-3d", transform: reduced ? "none" : rotate(REST.x, REST.y) }}>
        {variant === "extrude" &&
          Array.from({ length: depth }, (_, i) => (
            <span
              key={i}
              style={{ position: "absolute", inset: 0, color: sideColor, transform: `translateZ(${-(i + 1) * 0.022}em)`, filter: i === depth - 1 ? "drop-shadow(0 0.08em 0.06em oklch(0 0 0 / 0.35))" : undefined }}
            >
              {text}
            </span>
          ))}
        {variant === "float" && (
          <span style={{ position: "absolute", inset: 0, color: "oklch(0 0 0 / 0.4)", filter: "blur(0.08em)", transform: "translateZ(-0.6em) translateY(0.25em) scale(0.98)" }}>{text}</span>
        )}
        {variant === "shadow" && (
          <span ref={shadow} style={{ position: "absolute", inset: 0, color: "oklch(0 0 0 / 0.35)", filter: "blur(0.04em)", transform: "translate(-0.08em, -0.02em) translateZ(-0.3em)" }}>
            {text}
          </span>
        )}
        <span style={{ position: "relative", display: "block", transform: "translateZ(0.5px)" }}>{text}</span>
      </span>
    </Tag>
  );
}

export default TiltText;
```

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