Cursor Text Scramble
Every character becomes a span; on mousemove, those whose center is within 60 px of the pointer receive a random character out of 69 and the color #a78bfa, then come back one by one 200 to 500 ms later — no CSS animation, only setTimeouts. The sold HTML delivers the sentence as plain text: the script produces 44 spans, one per character.
You'll get access to the interactive demo with a free account.
This effect is part of Effect.Labs — 811 vanilla effects, some free, some premium. Cursors has 26 effects, including 1 free. Explore the category →
3 usage examples



How it works
On load, for each [data-text-scramble], the script takes the first .scramble-text, reads its textContent, empties it and recreates one span.char per character, with the original stored in data-original. The letters' CSS: display: inline-block (so each has a measurable box), transition: color .1s, and .char.scrambling { color: #a78bfa }. The text itself is 1.3 rem bold, letter-spacing: 2px, #e2e8f0, user-select: none, centered in a flex zone with padding: 30px.
On every mousemove over the zone, the script walks all the letters, measures each one (getBoundingClientRect) and computes the distance from its center to the pointer. If it is under 60 px and the original isn't a space, the letter gets the scrambling class and a character drawn from a 69-sign string (A–Z, a–z, 0–9, !@#$%&*). Its return timer is cleared then re-armed: setTimeout(…, 200 + 300 × Math.random()) — the letter recovers data-original and loses the class 200 to 500 ms after the pointer's last pass. As long as the mouse moves within range, each event draws a new character: that is the flicker.
Measured in Chromium: after a sweep across the text, the last character returns 497 ms after the last movement and the sentence is restored identically. There is no requestAnimationFrame and no keyframe: the only animation is the 100 ms color transition. The cost is the measuring: one geometry read per letter per event — negligible for 44 letters, worth watching on a paragraph. The native pointer stays displayed (cursor: default).
The sold HTML delivers the sentence as plain text inside .scramble-text: textContent holds exactly the 44 characters and the script creates 44 spans, one per letter and per space. That is the working condition, since the script re-reads textContent and re-splits everything, and the condition only spares the space " ". Indented HTML — the sentence pre-split into 44 span.char elements, each on its own line, as in an older extraction — made it count 45 line breaks and 272 indentation spaces, i.e. 357 spans instead of 44 (317 of zero width); within reach of the pointer, the line breaks received a visible character — measured on that HTML: 7 scrambled at once, stray letters between the words (“C U R S O kI D vL hM…”) and a line widening from 637 to 684 px.
Accessibility
- prefers-reduced-motion not handled: the letter flicker isn't a CSS animation, no media query stops it. Gate the
mousemovelistener on!matchMedia('(prefers-reduced-motion: reduce)').matches— the text then stays a text, readable and stable, which is the expected fallback. - The effect is mouse-only; on touch, at most one event per tap, with no consequence. The text carries no extra information while scrambled — it loses some, temporarily: don't apply it to an instruction that must be read at the very moment the mouse passes (menu, button label).
user-select: nonealso prevents selecting and copying the sentence: remove it if this is content. - Contrast:
#e2e8f0on#0a0a0f= 16:1 and the scramble color#a78bfa= 7.3:1, AAA. On a light page those values collapse (1.2:1 and 2.7:1 on white): switch the text to#1e293b(14.6:1) and the scramble color to#6d28d9(7.1:1). - Screen readers: the sentence is split into dozens of
inline-blockspans, and its content changes dozens of times per second on hover. Some readers spell inline-blocks one by one; all will read random characters if the user moves the mouse at the same time. Give.scramble-textanaria-labelcarrying the sentence (or a visually hidden copy) andaria-hidden="true"on the spans, so the spoken version is always the real one. - Integration: the HTML must provide the sentence as plain text inside
.scramble-text, as the sold code does — pre-split, indented spans would produce stray whitespace spans. The split happens once on load: text changed afterwards (translation, CMS) isn't picked up, re-run the script after. The zone needs a height —height: 100%inside the sold container yields 93 px, the text band plus 30 px of padding — and the pointer must pass within 60 px of the letters' centers.
Browser compatibility
ES5 code (var, function): it requires getBoundingClientRect, classList, setTimeout and data-*. No CSS animation apart from a color transition. Zero dependencies.
Without JavaScript, the sentence displays as is, as plain text or in its spans: nothing missing, nothing moving. Without CSS transitions, the violet color appears and disappears abruptly.
The code
HTML structure to paste into your page (CSS + JS available with a premium account):
<div class="scramble-zone" data-text-scramble>
<div class="scramble-text">CURSOR ALCHEMY TRANSFORMS DIGITAL EXPERIENCE</div>
</div>
Full HTML + CSS + JS, copy-paste ready — with hundreds of premium effects.
Customize
Options passed to the API or data-* attributes:
| Option / property | Default | Effect |
|---|---|---|
Action radius (JS — < 60) |
60 px | Distance from the pointer to a letter's center below which it scrambles. 30 touches one or two letters; 120 scrambles a whole word. At large font sizes, scale it up accordingly. |
| Character set (JS — 69-sign string) | A–Z a–z 0–9 !@#$%&* | Characters drawn at random. Careful: the index is Math.floor(69 × Math.random()) — if you change the string, put its length in place of 69, otherwise out-of-range indexes return undefined, displayed as is. |
Return delay (JS — 200 + 300 × Math.random()) |
200 to 500 ms | Time between the pointer's last pass and the letter's return. The timer is reset on every event: the letter only returns once the pointer has left. 80 + 120 × … = quick return; 600 + 900 × … = long trail. |
Scramble color (CSS — .char.scrambling) |
#a78bfa | Color of the letters while scrambled. The switch runs through transition: color .1s. Check 4.5:1 on your background (7.3:1 here on #0a0a0f). |
Spaces (JS — " " !== data-original) |
only spaces are spared | Replace the comparison with !/\s/.test(original) to also spare line breaks and tabs — essential if your HTML is indented or spans several lines. Spare punctuation too by adding it to the test. |
Typography (CSS — .scramble-text) |
1.3 rem, 700, 2 px letter-spacing | The larger the font, the fewer letters fit within the 60 px radius: adjust the radius with the size. A monospaced font prevents the line from shifting when a narrow letter (i, l) becomes a wide one (W, @). |
Target (HTML — .scramble-text, plain text) |
first .scramble-text of the zone | The script only processes the first .scramble-text of each [data-text-scramble] zone. Provide the sentence as plain text, without spans or indentation; for several titles, several zones. |
FAQ
textContent of .scramble-text and creates one span per character, line breaks and indentation included — and the condition only spares the space " ". The sold code delivers the sentence as plain text on a single line (<div class="scramble-text">CURSOR ALCHEMY …</div>, 44 spans). If you rewrote it over several indented lines, or if a tool froze the DOM into 44 neatly formatted span.char elements, every line break "\n" becomes a span that scrambles like a letter and shows a visible character where there was nothing — measured on such HTML: 357 spans instead of 44, signs between the words and a line going from 637 to 684 px. Two remedies, your pick: keep the sentence as plain text on one line, or replace the comparison with !/\s/.test(original).mousemove draws a new character for every letter within reach and resets its return timer (clearTimeout then setTimeout). As long as the pointer stays within 60 px, the letter flickers and its return is pushed back; as soon as it moves away, the last timer set runs out — 200 to 500 ms, drawn at random per letter, hence the scattered return. Measured: 497 ms between the last movement and the last restored letter. For a synchronized return, replace the random term with a constant.[data-text-scramble] zone per title, each with its own .scramble-text — the script only looks at the first one in each zone. Translation or content loaded afterwards: the split into spans happens once on load, on the text present at that moment; if you replace the text later (client-side i18n, CMS), the spans the script remembers no longer exist and hovering does nothing. Re-run the initialization function after writing the text, or render the final sentence server-side.