# Variable proximity

> Weight and width swell near the cursor.

- Type: Component (cursor)
- Page: https://textlab.dev/components/variable-proximity
- Markdown: https://textlab.dev/components/variable-proximity.md
- Registry item: https://textlab.dev/r/variable-proximity.json
- Source file: `registry/textlab/variable-proximity/variable-proximity.tsx`
- Dependencies: none
- Variants: weight, width, slant
- Keywords: variable font, font weight, proximity, cursor, wght, wdth, roboto flex

## When to use

- Type foundries, design tools and editorial heroes.
- Showing off a variable font.

## When not to use

- Fonts without the axis; nothing will change.
- Layouts that cannot tolerate letters changing width.

## Installation

### CLI

```bash
npx shadcn@latest add @textlab/variable-proximity
pnpm dlx shadcn@latest add @textlab/variable-proximity
```

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/variable-proximity.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 { VariableProximity } from "@/components/textlab/variable-proximity/variable-proximity";

export function Example() {
  return <VariableProximity as="h1" text="Variable type" variant="width" />;
}
```

## API reference

### VariableProximity

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `text` (required) | `string` | – | The copy. |
| `variant` | `"weight" \| "width" \| "slant"` | `"weight"` | Which variable axis responds. |
| `from` | `number` | `axis default` | Axis value far from the cursor. |
| `to` | `number` | `axis default` | Axis value under the cursor. |
| `radius` | `number` | `3` | Reach in em. |
| `font` | `string` | `var(--tl-font-flex)` | A variable font with the axis. Roboto Flex has weight, width and slant. |
| `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 text is read once.
- Weight and width change the letters' footprint, so neighbours move slightly; keep it to headlines.

## Motion

- Each letter eases toward its own target with damping.
- Updates stop once every letter has settled.

## Reduced motion

Static text at the resting axis value.

## Performance

- Writes font-variation-settings only while values change; text reflow is limited to the line.

## 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: weight, width, slant.

## Related

- [Breathing text](https://textlab.dev/components/breathing-text.md): Weight breathes in and out over time.
- [Magnetic text](https://textlab.dev/components/magnetic-text.md): Letters lean toward the cursor and spring back.
- [Spotlight text](https://textlab.dev/components/spotlight-text.md): A light follows the cursor and reveals the copy.

## Also in 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.
- [Tilt text](https://textlab.dev/components/tilt-text.md): Extruded type tilts toward the pointer.
- [Extrude text](https://textlab.dev/components/extrude-text.md): Depth grows from the letters on hover.

## Source

### components/textlab/variable-proximity/variable-proximity.tsx

```tsx
"use client";

import { useRef, type CSSProperties, type ElementType } from "react";
import { SrText } from "../lib/a11y";
import { damp } from "../lib/math";
import { splitWords } from "../lib/split";
import { useFrame } from "../lib/use-frame";
import { useLetterBoxes } from "../lib/use-letter-boxes";
import { usePointer } from "../lib/use-pointer";
import { useReduced, type ReducedMotion } from "../lib/use-reduced";

export type VariableProximityVariant = "weight" | "width" | "slant";

export interface VariableProximityProps {
  text: string;
  /** Which axis swells near the cursor. */
  variant?: VariableProximityVariant;
  /** Axis value far from the cursor and right under it. */
  from?: number;
  to?: number;
  /** Reach in em. */
  radius?: number;
  /** Variable font family. Needs the axis you animate. Defaults to the --tl-font-flex token. */
  font?: string;
  as?: ElementType;
  reducedMotion?: ReducedMotion;
  className?: string;
  style?: CSSProperties;
}

const AXIS: Record<VariableProximityVariant, { tag: string; from: number; to: number }> = {
  weight: { tag: "wght", from: 300, to: 900 },
  width: { tag: "wdth", from: 75, to: 151 },
  slant: { tag: "slnt", from: 0, to: -10 },
};

/**
 * Weight, width or slant swell as the cursor comes near. Each letter reads its own distance, so a
 * bulge of heavier type follows the pointer across the word.
 */
export function VariableProximity({ text, variant = "weight", from, to, radius = 3, font = "var(--tl-font-flex)", as: Tag = "span", reducedMotion = "user", className, style }: VariableProximityProps) {
  const root = useRef<HTMLElement | null>(null);
  const reduced = useReduced(reducedMotion);
  const pointer = usePointer(root, { enabled: !reduced });
  const { boxes, em } = useLetterBoxes(root, [text, variant]);
  const values = useRef<number[]>([]);
  const axis = AXIS[variant];
  const low = from ?? axis.from;
  const high = to ?? axis.to;
  const words = splitWords(text);

  useFrame(
    (_, dt) => {
      const p = pointer.current;
      const reach = radius * em.current;
      boxes.current.forEach((box, i) => {
        let target = low;
        if (p.active) {
          const dist = Math.hypot(p.x - box.cx, p.y - box.cy);
          const t = Math.max(0, 1 - dist / reach);
          target = low + (high - low) * t * t * (3 - 2 * t);
        }
        const prev = values.current[i] ?? low;
        const next = damp(prev, target, 12, dt);
        if (Math.abs(next - prev) < 0.05 && Math.abs(next - target) < 0.05) return;
        values.current[i] = next;
        box.el.style.fontVariationSettings = `"${axis.tag}" ${next.toFixed(1)}`;
      });
    },
    { ref: root, enabled: !reduced },
  );

  const base: CSSProperties = { fontFamily: font, fontVariationSettings: `"${axis.tag}" ${low}`, ...style };
  if (reduced) {
    return (
      <Tag className={className} style={base}>
        {text}
      </Tag>
    );
  }

  return (
    <Tag ref={root} className={className} style={{ position: "relative", display: "inline-block", ...base }}>
      <SrText>{text}</SrText>
      <span aria-hidden="true">
        {words.map((word, w) => (
          <span key={w}>
            <span style={{ whiteSpace: "nowrap" }}>
              {word.chars.map((char, c) => (
                <span key={c} data-tl-char="" style={{ fontVariationSettings: `"${axis.tag}" ${low}` }}>
                  {char}
                </span>
              ))}
            </span>
            {w < words.length - 1 ? " " : null}
          </span>
        ))}
      </span>
    </Tag>
  );
}

export default VariableProximity;
```

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