# Jelly text

> Letters wobble like jelly when touched.

- Type: Component (physics)
- Page: https://textlab.dev/components/jelly-text
- Markdown: https://textlab.dev/components/jelly-text.md
- Registry item: https://textlab.dev/r/jelly-text.json
- Source file: `registry/textlab/jelly-text/jelly-text.tsx`
- Dependencies: none
- Variants: squash, wobble
- Keywords: jelly, wobble, squash and stretch, springy, hover, bouncy

## When to use

- Playful brands, games and empty states.
- Words that invite poking, on mouse or touch.

## When not to use

- Interactive controls; letters are not buttons.
- Body text.

## Installation

### CLI

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

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

## API reference

### JellyText

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `text` (required) | `string` | – | The copy. |
| `variant` | `"squash" \| "wobble"` | `"squash"` | Squish flat and spring tall, or rock side to side. |
| `strength` | `number` | `1` | How hard a touch kicks the letter. |
| `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.
- Works with touch through pointer down.

## Motion

- Underdamped springs ring out over about a second.
- Neighbours get a smaller kick so the wobble spreads.

## Reduced motion

Touch does nothing; plain text.

## Performance

- The frame loop runs only while a spring is moving.

## 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: squash, wobble.

## Related

- [Elastic text](https://textlab.dev/components/elastic-text.md): Drag a letter and the word stretches like rubber.
- [Magnetic text](https://textlab.dev/components/magnetic-text.md): Letters lean toward the cursor and spring back.
- [Letter drop](https://textlab.dev/components/letter-drop.md): Letters fall, bounce and settle on the floor.

## Also in physics

- [Wave text](https://textlab.dev/components/wave-text.md): A sine wave runs through the line.
- [Breathing text](https://textlab.dev/components/breathing-text.md): Weight breathes in and out over time.

## Source

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

```tsx
"use client";

import { useRef, type CSSProperties, type ElementType } from "react";
import { SrText } from "../lib/a11y";
import { Letters } from "../lib/letters";
import { atRest, stepSpring, type Spring1D } from "../lib/physics";
import { useFrame } from "../lib/use-frame";
import { useReduced, type ReducedMotion } from "../lib/use-reduced";

export type JellyTextVariant = "wobble" | "squash";

export interface JellyTextProps {
  text: string;
  /** wobble rocks letters side to side, squash squishes them flat then springs them tall. */
  variant?: JellyTextVariant;
  /** How hard a touch kicks the letter. */
  strength?: number;
  as?: ElementType;
  reducedMotion?: ReducedMotion;
  className?: string;
  style?: CSSProperties;
}

/**
 * Letters wobble like jelly when touched. Every letter is an underdamped spring; brushing one
 * kicks it, and its neighbours feel a smaller kick.
 */
export function JellyText({ text, variant = "squash", strength = 1, as: Tag = "span", reducedMotion = "user", className, style }: JellyTextProps) {
  const root = useRef<HTMLElement | null>(null);
  const reduced = useReduced(reducedMotion);
  const springs = useRef<Spring1D[]>([]);
  const live = useRef(false);

  const kick = (index: number) => {
    if (reduced) return;
    const els = root.current?.querySelectorAll("[data-tl-char]").length ?? 0;
    for (let i = 0; i < els; i++) springs.current[i] ??= { x: 0, v: 0 };
    [-1, 0, 1].forEach((d) => {
      const s = springs.current[index + d];
      if (s) s.v += (d === 0 ? 9 : 4) * strength * (variant === "wobble" ? 1.4 : 1);
    });
    live.current = true;
  };

  useFrame(
    (_, dt) => {
      if (!live.current) return;
      const els = root.current?.querySelectorAll<HTMLElement>("[data-tl-char]");
      if (!els) return;
      let moving = false;
      springs.current.forEach((s, i) => {
        if (!s) return;
        stepSpring(s, 0, 260, 7, dt);
        if (!atRest(s, 0, 0.002)) moving = true;
        const el = els[i];
        if (!el) return;
        el.style.transform =
          variant === "wobble" ? `rotate(${(s.x * 18).toFixed(2)}deg)` : `scale(${(1 + s.x * 0.35).toFixed(3)}, ${(1 - s.x * 0.35).toFixed(3)})`;
      });
      live.current = moving;
    },
    { ref: root, enabled: !reduced },
  );

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

  return (
    <Tag ref={root} className={className} style={{ display: "inline-block", ...style }}>
      <SrText>{text}</SrText>
      <span aria-hidden="true">
        <Letters
          text={text}
          charStyle={{ transformOrigin: variant === "wobble" ? "50% 100%" : "50% 100%", willChange: "transform" }}
          render={(char, i) => (
            <span style={{ display: "inline-block" }} onPointerEnter={() => kick(i)} onPointerDown={() => kick(i)}>
              {char}
            </span>
          )}
        />
      </span>
    </Tag>
  );
}

export default JellyText;
```

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