# Scroll stretch

> Letters stretch tall as you scroll fast.

- Type: Component (scroll)
- Page: https://textlab.dev/components/scroll-stretch
- Markdown: https://textlab.dev/components/scroll-stretch.md
- Registry item: https://textlab.dev/r/scroll-stretch.json
- Source file: `registry/textlab/scroll-stretch/scroll-stretch.tsx`
- Dependencies: motion
- Variants: vertical, horizontal
- Keywords: stretch, scroll velocity, taffy, elastic, squash

## When to use

- Short, heavy display words on playful pages.
- Section dividers that react to momentum.

## When not to use

- Long titles; per-letter springs add up.
- Serious, text-first pages.

## Installation

### CLI

```bash
npx shadcn@latest add @textlab/scroll-stretch
pnpm dlx shadcn@latest add @textlab/scroll-stretch
```

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/scroll-stretch.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 { ScrollStretch } from "@/components/textlab/scroll-stretch/scroll-stretch";

export function Example() {
  return <ScrollStretch as="h2" text="Elastic" />;
}
```

## API reference

### ScrollStretch

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `text` (required) | `string` | – | The copy. |
| `variant` | `"vertical" \| "horizontal"` | `"vertical"` | Pull letters tall, or smear them sideways. |
| `max` | `number` | `0.6` | Largest stretch above 1. |
| `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

- Letters are aria-hidden; the word is read once.
- Moves only during fast scrolling.

## Motion

- Each letter has its own spring, slightly softer along the word, so the stretch ripples.
- Volume is preserved: taller letters get narrower.

## Reduced motion

Still, normal letters.

## Performance

- One spring per letter. Keep it to short words.

## 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: vertical, horizontal.

## Related

- [Scroll skew](https://textlab.dev/components/scroll-skew.md): Type leans with the speed of your scroll.
- [Elastic text](https://textlab.dev/components/elastic-text.md): Drag a letter and the word stretches like rubber.
- [Velocity marquee](https://textlab.dev/components/velocity-marquee.md): A marquee that speeds up, slows and reverses with scroll.

## Also in scroll

- [Scroll fill](https://textlab.dev/components/scroll-fill.md): Words light up as you read down the page.
- [Depth layers](https://textlab.dev/components/depth-layers.md): Outline copies drift apart in depth as you scroll.

## Source

### components/textlab/scroll-stretch/scroll-stretch.tsx

```tsx
"use client";

import type { CSSProperties, ElementType, RefObject } from "react";
import { motion, useSpring, useTransform, type MotionValue } from "motion/react";
import { SrText } from "../lib/a11y";
import { clamp } from "../lib/math";
import { graphemes } from "../lib/split";
import { useReduced, type ReducedMotion } from "../lib/use-reduced";
import { useScrollVelocity } from "../lib/use-scroll-velocity";

export type ScrollStretchVariant = "vertical" | "horizontal";

export interface ScrollStretchProps {
  text: string;
  /** vertical pulls letters tall like taffy; horizontal smears them sideways. */
  variant?: ScrollStretchVariant;
  /** Largest stretch above 1. */
  max?: number;
  container?: RefObject<HTMLElement | null>;
  as?: ElementType;
  reducedMotion?: ReducedMotion;
  className?: string;
  style?: CSSProperties;
}

function Letter({ char, amount, index, total, variant }: { char: string; amount: MotionValue<number>; index: number; total: number; variant: ScrollStretchVariant }) {
  // Letters further along respond a little later, so the stretch travels through the word like a wave.
  const lagged = useSpring(amount, { stiffness: 520 - (index / Math.max(1, total)) * 320, damping: 26 });
  const scale = useTransform(lagged, (v) => 1 + v);
  const counter = useTransform(lagged, (v) => 1 / Math.sqrt(1 + v));
  return (
    <motion.span
      style={{
        display: "inline-block",
        whiteSpace: "pre",
        transformOrigin: variant === "vertical" ? "50% 100%" : "0% 50%",
        scaleY: variant === "vertical" ? scale : counter,
        scaleX: variant === "vertical" ? counter : scale,
      }}
    >
      {char}
    </motion.span>
  );
}

/** Letters stretch as you scroll fast and snap back when you stop, each one a beat after the last. */
export function ScrollStretch({ text, variant = "vertical", max = 0.6, container, as: Tag = "span", reducedMotion = "user", className, style }: ScrollStretchProps) {
  const reduced = useReduced(reducedMotion);
  const velocity = useScrollVelocity(container);
  const amount = useTransform(velocity, (v) => clamp(Math.abs(v) / 3000, 0, max));
  const chars = graphemes(text);

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

  return (
    <Tag className={className} style={{ display: "inline-block", ...style }}>
      <SrText>{text}</SrText>
      <span aria-hidden="true" style={{ display: "inline-block", whiteSpace: "nowrap" }}>
        {chars.map((char, i) => (
          <Letter key={i} char={char} amount={amount} index={i} total={chars.length} variant={variant} />
        ))}
      </span>
    </Tag>
  );
}

export default ScrollStretch;
```

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