# Hover scramble

> Scrambles on hover, settles on leave.

- Type: Component (typing)
- Page: https://textlab.dev/components/hover-scramble
- Markdown: https://textlab.dev/components/hover-scramble.md
- Registry item: https://textlab.dev/r/hover-scramble.json
- Source file: `registry/textlab/hover-scramble/hover-scramble.tsx`
- Dependencies: none
- Variants: sweep, all
- Keywords: hover, scramble, nav link, button, glitch hover

## When to use

- Navigation links, footers and small buttons on technical brands.
- Any short label where hover feedback should feel playful.

## When not to use

- Primary calls to action; the label is briefly unreadable.
- Touch-first interfaces, where hover never happens.

## Installation

### CLI

```bash
npx shadcn@latest add @textlab/hover-scramble
pnpm dlx shadcn@latest add @textlab/hover-scramble
```

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/hover-scramble.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 { HoverScramble } from "@/components/textlab/hover-scramble/hover-scramble";

export function Example() {
  return <a href="/work"><HoverScramble text="Selected work" /></a>;
}
```

## API reference

### HoverScramble

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `text` (required) | `string` | – | The copy. |
| `variant` | `"sweep" \| "all"` | `"sweep"` | sweep re-settles left to right; all snaps back together at the end. |
| `charset` | `string` | `symbols` | Glyphs used while scrambled. |
| `duration` | `number` | `0.5` | Seconds each part takes. |
| `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

- Keyboard focus triggers it too, through focus capture on the wrapper.
- Screen readers hear the label, never the glyphs.

## Motion

- Half a second, under the 300 ms-per-step budget for UI feedback.
- Leaving mid-scramble settles straight back to the word.

## Reduced motion

Hover does nothing; the label stays still.

## Performance

- Runs only while hovered.
- No layout work; the real label holds the size.

## 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: sweep, all.

## Related

- [Scramble text](https://textlab.dev/components/scramble-text.md): Letters decode into place.
- [Flip roll](https://textlab.dev/components/flip-roll.md): Hover and the label rolls up into a fresh copy.
- [Magnetic text](https://textlab.dev/components/magnetic-text.md): Letters lean toward the cursor and spring back.

## Also in typing

- [Typewriter](https://textlab.dev/components/typewriter.md): Types, deletes and retypes with a caret.
- [Matrix decode](https://textlab.dev/components/matrix-decode.md): Glyph rain resolves into the word.
- [Text morph](https://textlab.dev/components/text-morph.md): One word turns into the next, letter by letter.
- [Word rotator](https://textlab.dev/components/word-rotator.md): Cycles words while the width follows.

## Source

### components/textlab/hover-scramble/hover-scramble.tsx

```tsx
"use client";

import type { CSSProperties, ElementType, ReactNode } from "react";
import { SrText } from "../lib/a11y";
import { charsets, useScramble } from "../lib/scramble";
import { useReduced, type ReducedMotion } from "../lib/use-reduced";
import { useTrigger } from "../lib/use-trigger";

export type HoverScrambleVariant = "sweep" | "all";

export interface HoverScrambleProps {
  text: string;
  /** sweep re-settles left to right; all stays scrambled, then snaps back together. */
  variant?: HoverScrambleVariant;
  charset?: string;
  /** Seconds the scramble lasts. */
  duration?: number;
  as?: ElementType;
  reducedMotion?: ReducedMotion;
  className?: string;
  style?: CSSProperties;
  /** Optional element to wrap, for example an icon after the label. */
  children?: ReactNode;
}

/**
 * Scrambles on hover or keyboard focus and settles back into the word. Made for nav links and
 * buttons: put it inside a focusable element and it follows that element's focus.
 */
export function HoverScramble({ text, variant = "sweep", charset = charsets.symbols, duration = 0.5, as: Tag = "span", reducedMotion = "user", className, style, children }: HoverScrambleProps) {
  const { ref, on, bind } = useTrigger<HTMLElement>({ trigger: "hover" });
  const reduced = useReduced(reducedMotion);
  const frame = useScramble(text, { active: on && !reduced, duration, charset, mode: variant, idle: "text", tick: 40 });

  return (
    <Tag
      ref={ref}
      className={className}
      style={{ position: "relative", display: "inline-block", ...style }}
      onPointerEnter={bind.onPointerEnter}
      onPointerLeave={bind.onPointerLeave}
      // Focus inside (for example the parent link) counts as hover.
      onFocusCapture={bind.onFocus}
      onBlurCapture={bind.onBlur}
    >
      <SrText>{text}</SrText>
      <span aria-hidden="true" style={{ visibility: "hidden" }}>
        {text}
      </span>
      <span aria-hidden="true" style={{ position: "absolute", inset: 0, whiteSpace: "pre" }}>
        {frame}
      </span>
      {children}
    </Tag>
  );
}

export default HoverScramble;
```

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