Scramble on Hover

Scramble on Hover makes the letters of a word flicker through random characters when the mouse moves over it, then settle on the real word. It adds a little life to links and buttons without changing what they say.

Hover over this sentence and every word scrambles in its own place.

import { ArrowUpRight } from "lucide-react";
import { Button } from "@/components/ui/button";
import { ScrambleOnHover } from "@/components/ui/scramble-on-hover";
import { focusRing } from "@/lib/styles";

const link = `rounded-sm text-3xl font-semibold uppercase text-text-secondary transition-colors hover:text-text ${focusRing}`;

export default function ScrambleOnHoverDemo() {
  return (
    <div className="flex w-full flex-col items-center gap-8">
      <div className="flex flex-wrap items-center justify-center gap-x-8 gap-y-4">
        {["Home", "Product", "Pricing"].map((label) => (
          <ScrambleOnHover key={label} render={<a href="#" className={link} />}>
            {label}
          </ScrambleOnHover>
        ))}
        <ScrambleOnHover
          render={<a href="#" className={`inline-flex items-center gap-1 ${link}`} />}
          after={<ArrowUpRight aria-hidden className="size-6" />}
        >
          GitHub
        </ScrambleOnHover>
      </div>
      <ScrambleOnHover render={<Button variant="secondary" />}>Get started</ScrambleOnHover>
      <ScrambleOnHover render={<p className="max-w-xs text-center text-lg text-text/80" />}>
        Hover over this sentence and every word scrambles in its own place.
      </ScrambleOnHover>
    </div>
  );
}

The code

The component is a single file, read from the repository when the site is built. Press Show code to see all of it.

"use client";

import type { ReactNode } from "react";
import { mergeProps } from "@base-ui/react/merge-props";
import { useRender } from "@base-ui/react/use-render";
import { ScrambledText, useScramble } from "@/lib/scramble";

export interface ScrambleOnHoverProps extends Omit<useRender.ComponentProps<"span">, "children"> {
  // The text. It must be a plain string.
  children: string;
  // The most time the effect takes, in milliseconds. A short word finishes sooner.
  duration?: number;
  // The characters the random letters are picked from.
  characters?: string;
  // Something inside the same element, before or after the text, such as an icon. It does not
  // scramble.
  before?: ReactNode;
  after?: ReactNode;
}

// When the mouse moves over it, the letters of the text flicker through random characters and
// settle on the real ones from left to right. It is a span by default; pass `render` to put the
// effect on your own element, such as a link or a Button, and the whole element reacts to the
// hover, not only the letters. Keyboard focus and touch do nothing, and neither does a visitor
// who asks for reduced motion.
export function ScrambleOnHover({
  children,
  duration = 700,
  characters,
  before,
  after,
  render,
  ...props
}: ScrambleOnHoverProps) {
  const { display, play, stop } = useScramble(children, { duration, characters });

  return useRender({
    defaultTagName: "span",
    render,
    props: mergeProps<"span">(
      {
        onPointerEnter: (event) => {
          if (event.pointerType === "mouse") play();
        },
        onPointerLeave: stop,
        children: (
          <>
            {before}
            <ScrambledText text={children} display={display} />
            {after}
          </>
        ),
      },
      props,
    ),
  });
}

Props

PropTypeDefaultWhat it does
childrenstring—The text. It must be a plain string. Required.
renderelement—Puts the effect on your own element, such as a link or a Button, instead of a span. The hover is read on that element, so the whole link reacts and not only its letters.
durationnumber700The most time the effect takes, in milliseconds. A short word finishes sooner, at about 80ms per letter.
charactersstringA–Z, 0–9 and #%&*+?The characters the random letters are picked from.
before / afterReactNode—Something inside the same element, before or after the text, such as an icon or a line. It does not scramble.

Every other prop goes to the element, a span or the one you pass as render.

The thinking

The effect is a flourish on something people already hover: a link, a button, a word in a menu. It should never be the reason they notice it is there.

  • One element does the work: It listens for the hover on itself. With render you choose which element that is, so a link can react across its whole row.
  • Nothing moves: The real word stays underneath, invisible, and holds the width. The random characters sit on top of it and are clipped to it, so the layout never shifts.
  • Words, not sentences: Every word keeps its own width, so a sentence that wraps still wraps in the same places while it flickers.
  • Left alone: Spaces and punctuation never change, and a lowercase letter is only ever replaced by a lowercase one.

Interface design

  • It is only the letters: Size, weight, capitals and color come from the element, so the effect matches whatever text it is on.
  • The noise: Capital letters, digits and a few symbols, which you can change with characters.
  • The timing: About a tenth of a second of pure noise, then the real letters settle from left to right, about 80ms apart. A long text is sped up so it never takes longer than duration.

Interaction

  • Hover: The effect starts when the mouse (or a pen) enters the element, and starts again if it leaves and comes back.
  • Leaving: If the mouse leaves in the middle of the effect, the real text comes back at once.
  • Keyboard and touch: Nothing happens on keyboard focus or on a tap, so the text is always steady for them.
  • Reduced motion: For people who ask their system for less motion, nothing flickers.

Empathy

  • Screen readers: They always get the real text, from a visually hidden copy. The flickering copy is hidden from them, so they never hear random letters.
  • Never the only signal: The effect carries no meaning. Keep your own hover style on the element, as the demo does with its color.
  • Use it sparingly: Moving letters draw the eye. A few links in a menu is enough; a whole page of them is tiring.
  • Short words read best: The effect is quick enough to read through, but a long sentence flickering under the pointer is harder to follow.