Scramble on Load
Scramble on Load makes the letters of a heading or a line of text flicker through random characters and settle on the real ones, as soon as the text is on the screen: when the page loads, or when you scroll to it.
Own every pixel
A line of text that starts a moment after the heading and settles from left to right.
"use client";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { ScrambleOnLoad } from "@/components/ui/scramble-on-load";
export default function ScrambleOnLoadDemo() {
// The effect plays once, so changing the key remounts the text to play it again.
const [run, setRun] = useState(0);
return (
<div className="flex w-full flex-col items-center gap-8">
<div key={run} className="flex flex-col items-center gap-3 text-center">
<ScrambleOnLoad render={<h2 className="text-4xl font-bold tracking-tight" />}>
Own every pixel
</ScrambleOnLoad>
<ScrambleOnLoad delay={500} render={<p className="max-w-sm text-lg text-text/80" />}>
A line of text that starts a moment after the heading and settles from left to right.
</ScrambleOnLoad>
</div>
<Button variant="secondary" onClick={() => setRun((value) => value + 1)}>
Replay
</Button>
</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 { useEffect, useRef, 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 ScrambleOnLoadProps 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;
// How long to wait, in milliseconds, after the text comes into view before it starts.
delay?: number;
// Plays only the first time the text comes into view. Pass false to play again every time it
// scrolls out of view and back.
once?: boolean;
// 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;
}
// The letters of the text flicker through random characters and settle on the real ones from
// left to right, as soon as it is on the screen: when the page loads, if it is already in view,
// or when it scrolls into view. It is a span by default; pass `render` to make it a heading or a
// paragraph. Nobody sees the effect if they ask for reduced motion.
export function ScrambleOnLoad({
children,
duration = 1200,
delay = 0,
once = true,
characters,
before,
after,
render,
...props
}: ScrambleOnLoadProps) {
const { display, play, stop } = useScramble(children, { duration, characters });
const element = useRef<HTMLElement>(null);
useEffect(() => {
const target = element.current;
if (!target) return;
let timer: number | undefined;
const observer = new IntersectionObserver(
(entries) => {
for (const entry of entries) {
if (entry.isIntersecting) {
timer = window.setTimeout(play, delay);
if (once) observer.disconnect();
} else if (!once) {
window.clearTimeout(timer);
stop();
}
}
},
{ threshold: 0.2 },
);
observer.observe(target);
return () => {
observer.disconnect();
window.clearTimeout(timer);
};
}, [play, stop, delay, once]);
return useRender({
defaultTagName: "span",
render,
ref: element,
props: mergeProps<"span">(
{
children: (
<>
{before}
<ScrambledText text={children} display={display} />
{after}
</>
),
},
props,
),
});
}Props
| Prop | Type | Default | What it does |
|---|---|---|---|
children | string | — | The text. It must be a plain string. Required. |
render | element | — | Makes it your own element, such as a heading or a paragraph, instead of a span. |
duration | number | 1200 | The most time the effect takes, in milliseconds. A short word finishes sooner, at about 80ms per letter. |
delay | number | 0 | How long to wait after the text comes into view before it starts, in milliseconds. |
once | boolean | true | Plays only the first time the text comes into view. Pass false to play it again every time it scrolls out of view and back. |
characters | string | A–Z, 0–9 and #%&*+? | The characters the random letters are picked from. |
before / after | ReactNode | — | 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
A heading that assembles itself is a way to say: this is the start. It works best once per page, on the one line you want read first.
- Starts when it is seen: It waits until at least a fifth of the text is on the screen, so a heading far down the page plays when you reach it, not while you cannot see it.
- Once by default: Scrolling back up does not replay it. Set once to false for text that should play every time it returns.
- Same parts as Scramble on Hover: The real text holds the width and the layout never moves, and every word keeps its own width. Spaces and punctuation are left alone.
- Headings and paragraphs: Pass render to make it an h1 or a p. A heading keeps its meaning for screen readers.
Interface design
- Only the letters: Size, weight and color come from the element you render, so it matches the page.
- Slower than hover: It takes up to 1.2 seconds by default, because it is a moment to watch and not a reaction to a pointer.
- Staggering: Give a second line a delay, as the demo does, and it starts after the first instead of at the same time.
Interaction
- Play: It plays as soon as the element is on the screen, which for text at the top of a page is right after the page loads.
- Scroll: Text below the fold stays steady until you scroll to it, then plays.
- Replay: With once set to false it plays again each time it comes back into view, and shows the real text while it is out of view.
- Reduced motion: For people who ask their system for less motion, the text is simply shown.
Empathy
- Screen readers: They get the real text from a visually hidden copy, and never hear the random letters.
- Before the scripts load: The real text is in the page from the start, so it can be read, searched and found before any effect runs.
- Easy to wait for: Keep it short and fast. People should never have to wait for a heading to finish before they can read it.
- One at a time: A few moving headings on a page are plenty. Too many make the page feel unsteady.