# Scroll skew

> Type leans with the speed of your scroll.

- Type: Component (scroll)
- Page: https://textlab.dev/components/scroll-skew
- Markdown: https://textlab.dev/components/scroll-skew.md
- Registry item: https://textlab.dev/r/scroll-skew.json
- Source file: `registry/textlab/scroll-skew/scroll-skew.tsx`
- Dependencies: motion
- Variants: skew, stretch
- Keywords: scroll velocity, skew on scroll, momentum, stretch, scroll effect

## When to use

- Big section titles on long, energetic pages.
- Portfolios with a lot of momentum.

## When not to use

- Reading-heavy pages; motion during scroll can distract.
- Small text.

## Installation

### CLI

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

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

export function Example() {
  return <ScrollSkew as="h2" text="Keep scrolling" />;
}
```

## API reference

### ScrollSkew

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `text` (required) | `string` | – | The copy. |
| `variant` | `"skew" \| "stretch"` | `"skew"` | Lean with scroll speed, or squash and stretch. |
| `max` | `number` | `12 (skew), 0.35 (stretch)` | Largest lean in degrees, or 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

- The text is never split; it reads normally.
- Motion only happens while the user is scrolling.

## Motion

- Velocity passes through a spring, so the lean rises and settles smoothly.
- Stretch preserves area, narrowing as it grows taller.

## Reduced motion

Upright, still text.

## Performance

- Transform only, driven by motion values; no React renders on scroll.

## 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: skew, stretch.

## Related

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

## Also in scroll

- [Depth layers](https://textlab.dev/components/depth-layers.md): Outline copies drift apart in depth as you scroll.

## Source

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

```tsx
"use client";

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

export type ScrollSkewVariant = "skew" | "stretch";

export interface ScrollSkewProps {
  text: string;
  /** skew leans the line with scroll speed; stretch squashes and stretches it vertically. */
  variant?: ScrollSkewVariant;
  /** Largest lean in degrees (skew) or stretch factor above 1 (stretch). */
  max?: number;
  container?: RefObject<HTMLElement | null>;
  as?: ElementType;
  reducedMotion?: ReducedMotion;
  className?: string;
  style?: CSSProperties;
}

/** Type leans with the speed of your scroll and springs upright when you stop. */
export function ScrollSkew({ text, variant = "skew", max, container, as: Tag = "span", reducedMotion = "user", className, style }: ScrollSkewProps) {
  const reduced = useReduced(reducedMotion);
  const velocity = useScrollVelocity(container);
  const limit = max ?? (variant === "skew" ? 12 : 0.35);
  const skewY = useTransform(velocity, (v) => (variant === "skew" ? clamp(v / 140, -limit, limit) : 0));
  const scaleY = useTransform(velocity, (v) => (variant === "stretch" ? 1 + clamp(Math.abs(v) / 4000, 0, limit) : 1));
  const scaleX = useTransform(scaleY, (s) => 1 / Math.sqrt(s));

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

  return (
    <Tag className={className} style={{ display: "inline-block", ...style }}>
      <motion.span style={{ display: "inline-block", skewY, scaleY, scaleX, transformOrigin: variant === "stretch" ? "50% 100%" : "50% 50%" }}>{text}</motion.span>
    </Tag>
  );
}

export default ScrollSkew;
```

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