# Extrude text

> Depth grows from the letters on hover.

- Type: Component (cursor)
- Page: https://textlab.dev/components/extrude-text
- Markdown: https://textlab.dev/components/extrude-text.md
- Registry item: https://textlab.dev/r/extrude-text.json
- Source file: `registry/textlab/extrude-text/extrude-text.tsx`
- Dependencies: none
- Variants: block, long-shadow, stack
- Keywords: 3d text, extrude, long shadow, retro, text shadow, block letters

## When to use

- Retro, poster and toy-like brands.
- Big words that should feel pressable.

## When not to use

- Small text; the depth smears it.
- Minimal, quiet interfaces.

## Installation

### CLI

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

export function Example() {
  return <ExtrudeText as="h2" text="Pop" variant="long-shadow" />;
}
```

## API reference

### ExtrudeText

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `text` (required) | `string` | – | The copy. |
| `variant` | `"block" \| "long-shadow" \| "stack"` | `"block"` | A solid slab, a fading diagonal shadow, or stacked outlines. |
| `depth` | `number` | `14` | Depth steps at full extrusion. |
| `color` | `string` | `var(--tl-accent)` | Depth color. |
| `hover` | `boolean` | `true` | Grow on hover from a shallow rest; false keeps it fully extruded. |
| `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

- Plain text with shadows; nothing is split.
- Focus-visible grows it too when the element is focusable.

## Motion

- The face lifts up and left as depth grows, so it feels like it rises off the page.
- 260 ms with the enter ease; hover only counts on devices with a fine pointer, so taps never leave it stuck raised.

## Reduced motion

Fully extruded and still.

## Performance

- Pure CSS: a registered property interpolates the shadow stack.

## 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: block, long-shadow, stack.

## Related

- [Tilt text](https://textlab.dev/components/tilt-text.md): Extruded type tilts toward the pointer.
- [Depth layers](https://textlab.dev/components/depth-layers.md): Outline copies drift apart in depth as you scroll.
- [Neon sign](https://textlab.dev/components/neon-text.md): A neon sign that hums and flickers on.

## 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/extrude-text/extrude-text.tsx

```tsx
"use client";

import type { CSSProperties, ElementType } from "react";
import { useReduced, type ReducedMotion } from "../lib/use-reduced";

export type ExtrudeTextVariant = "block" | "long-shadow" | "stack";

export interface ExtrudeTextProps {
  text: string;
  /** block builds a solid slab, long-shadow casts a fading diagonal, stack offsets outlined copies. */
  variant?: ExtrudeTextVariant;
  /** Number of depth steps at full extrusion. */
  depth?: number;
  /** Depth color. */
  color?: string;
  /** Hover grows the depth from flat; false keeps it extruded. */
  hover?: boolean;
  as?: ElementType;
  reducedMotion?: ReducedMotion;
  className?: string;
  style?: CSSProperties;
}

const CSS = `
@property --tl-depth { syntax: "<number>"; inherits: true; initial-value: 0; }
.tl-extrude { --tl-depth: var(--tl-depth-rest); transition: --tl-depth 260ms var(--tl-ease-enter, cubic-bezier(0.23, 1, 0.32, 1)), transform 260ms var(--tl-ease-enter, cubic-bezier(0.23, 1, 0.32, 1)); }
.tl-extrude[data-hover]:focus-visible { --tl-depth: 1; }
@media (hover: hover) and (pointer: fine) { .tl-extrude[data-hover]:hover { --tl-depth: 1; } }
@media (prefers-reduced-motion: reduce) { .tl-extrude[data-motion="user"] { transition: none; } }
`;

function layers(variant: ExtrudeTextVariant, depth: number, color: string) {
  return Array.from({ length: depth }, (_, i) => {
    const n = i + 1;
    const off = `calc(var(--tl-depth) * ${(n * 0.012).toFixed(3)}em)`;
    if (variant === "long-shadow") {
      return `${off} ${off} 0 color-mix(in oklab, ${color} ${Math.round(100 - (n / depth) * 85)}%, transparent)`;
    }
    if (variant === "stack") {
      // Every third step draws a ring, which reads as stacked outlines.
      return n % 3 === 0 ? `${off} ${off} 0 ${color}` : `${off} ${off} 0 transparent`;
    }
    return `${off} ${off} 0 ${color}`;
  }).join(", ");
}

/**
 * Depth grows from the letters on hover. The extrusion is a registered CSS property, so the browser
 * interpolates it smoothly with no JavaScript at all.
 */
export function ExtrudeText({ text, variant = "block", depth = 14, color = "var(--tl-accent, #3a9a5b)", hover = true, as: Tag = "span", reducedMotion = "user", className, style }: ExtrudeTextProps) {
  const reduced = useReduced(reducedMotion);
  const rest = hover && !reduced ? 0.25 : 1;
  return (
    <Tag
      className={["tl-extrude", className].filter(Boolean).join(" ")}
      data-hover={hover && !reduced ? "" : undefined}
      data-motion={reducedMotion === "user" ? "user" : undefined}
      style={{
        display: "inline-block",
        "--tl-depth-rest": rest,
        textShadow: layers(variant, depth, color),
        transform: "translate(calc(var(--tl-depth) * -0.06em), calc(var(--tl-depth) * -0.06em))",
        ...style,
      } as CSSProperties}
    >
      <style href="tl-extrude-text" precedence="default">{CSS}</style>
      {text}
    </Tag>
  );
}

export default ExtrudeText;
```

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