Skip to content
← Components

Scramble Text

Scramble Text brings controlled expression to your interface.

Open ↗

Make it yours.

Install the editable source and its dependencies into your React + Tailwind project.

Scroll horizontally for long lines
pnpm dlx shadcn@4.0.8 add http://localhost:3000/r/scramble-text.json

Use it

Scroll horizontally for long lines
"use client";
import { ScrambleText } from "@/components/jez-ui/ui/scramble-text";

export default function Example() {
  return (
    <>
      <div className="w-full rounded-xl bg-foreground p-8 text-accent md:p-14">
        <ScrambleText className="font-display text-4xl font-medium tracking-tight md:text-6xl">
          Expect the unexpected.
        </ScrambleText>
        <div className="mt-10 flex justify-between border-t border-current pt-4 text-xs">
          <span>Order from a little chaos.</span>
          <span>Jez / Motion</span>
        </div>
      </div>
    </>
  );
}

Props & types

Scroll horizontally for long lines
Omit<
  React.ComponentProps<"span">,
  keyof {
    children: string;
    className?: string;
  }
> & {
  children: string;
  className?: string;
}

Source

View 2 source files

registry/ui/scramble-text.tsx

Scroll horizontally for long lines
"use client";
import * as React from "react";
import { useReducedMotion } from "motion/react";
import { cn } from "./utils";
export function ScrambleText({
  children,
  className,
  ...rootProps
}: Omit<
  React.ComponentProps<"span">,
  keyof {
    children: string;
    className?: string;
  }
> & {
  children: string;
  className?: string;
}) {
  const [text, setText] = React.useState(children);
  const reduce = useReducedMotion();
  const timer = React.useRef<ReturnType<typeof setInterval> | null>(null);
  React.useEffect(() => {
    setText(children);
    return () => {
      if (timer.current) clearInterval(timer.current);
    };
  }, [children, reduce]);
  function run() {
    if (reduce) return;
    if (timer.current) clearInterval(timer.current);
    let step = 0;
    timer.current = setInterval(() => {
      step++;
      setText(
        Array.from(children)
          .map((c, i) =>
            i < step / 2 || /\s/.test(c)
              ? c
              : "ABCDEFGHIJKLMNOPQRSTUVWXYZ"[Math.floor(Math.random() * 26)],
          )
          .join(""),
      );
      if (step >= children.length * 2 && timer.current)
        clearInterval(timer.current);
    }, 28);
  }
  let offset = 0;
  return (
    <span
      {...rootProps}
      tabIndex={0}
      aria-label={children}
      onPointerEnter={(event) => {
        rootProps.onPointerEnter?.(event);
        if (!event.defaultPrevented) run();
      }}
      onFocus={(event) => {
        rootProps.onFocus?.(event);
        if (!event.defaultPrevented) run();
      }}
      className={cn("inline-block", className)}
    >
      {children.split(/(\s+)/).map((word, w) => {
        const start = offset;
        offset += word.length;
        return /\s/.test(word) ? (
          <React.Fragment key={w}>{word}</React.Fragment>
        ) : (
          <span
            key={w}
            className="inline-flex whitespace-nowrap"
            aria-hidden="true"
          >
            {Array.from(word).map((char, i) => (
              <span key={i} className="relative inline-block">
                <span className="invisible">{char}</span>
                <span className="absolute inset-0 text-center">
                  {text[start + i] ?? char}
                </span>
              </span>
            ))}
          </span>
        );
      })}
    </span>
  );
}

registry/ui/utils.ts

Scroll horizontally for long lines
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...values: ClassValue[]) {
  return twMerge(clsx(values));
}

Dependencies

motion@13.2.0 · clsx@2.1.1 · tailwind-merge@3.5.0

Accessibility & behaviour

Respect reduced-motion preferences. Decorative effects must not carry essential information. WebGL scenes include a static fallback and only render while visible.

Read the accessibility and performance guide →