Text Shuffle Reveal
Each letter races through random substitutions before locking onto the correct value — a character-by-character decoder effect, 100% vanilla.
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. Text has 56 effects, including 4 free. Explore the category →
3 usage examples



How it works
The .kt-text.kt-shuffle container holds a data-text attribute defining the target string. The splitChars() function reads that value, clears the element, and rebuilds one <span class="char" data-char="X">?</span> per character: the target is archived in data-char, and the initial textContent is a random character.
On each tick of a setInterval (≈ 50 ms), every span receives a new character drawn from the CHARS pool (A–Z + a–z, digits, symbols). A per-position counter limits substitutions to CYCLES_PER_CHAR iterations; once reached, the interval is cleared and the span displays its data-char value at full white opacity.
A staggered start delay (STAGGER, ≈ 60 ms per position) offsets each character's animation via setTimeout — the first letter locks before the last one has even started cycling, producing the left-to-right decoding cascade the effect is known for.
A click listener on the container re-invokes shuffleReveal() — every span reverts to a random half-opacity character and the cycle restarts from zero. splitChars() rebuilds the span structure if textContent was modified between calls.
Accessibility
- No
@media (prefers-reduced-motion: reduce)block in the provided code — users who have enabled this OS preference see the cycling animation run normally. Fix: testmatchMedia('(prefers-reduced-motion: reduce)').matchesat start-up and, if true, show thedata-charvalues directly without launching any intervals. - In-transit
.charspans carry noaria-hidden— a screen reader may announce the intermediate random letters. Fix: addaria-hidden="true"to the animated container and provide a separate<span class="sr-only">element holding the target text. - No
roleoraria-labelattribute on the container by default — addaria-label="target text"andaria-live="off"if the container is the sole carrier of meaningful textual content. - The
clicktrigger is not keyboard-accessible (notabindex, nokeydownlistener) — addtabindex="0"and listen forEnter/Spaceto serve keyboard users. - In-transit characters are rendered in
rgba(255,255,255,0.5)on a#0a0a0fbackground (ratio ≈ 3.5:1, below the WCAG AA threshold of 4.5:1). Acceptable for a short animation on a dark ground; reinforce if the cycle duration is long or the background lighter.
Browser compatibility
Requires DOM Level 2, setInterval, and addEventListener — available in all modern browsers since 2012.
With JS disabled, <code>.char</code> spans display their pre-generated random characters — the content is not readable. Provide a <code><noscript></code> fallback or an <code>aria-label</code> on the container to keep the text accessible.
The code
HTML structure to paste into your page (CSS + JS available with a premium account):
<div class="kt-text kt-shuffle" id="shuffle1" data-text="YOUR TEXT" style="cursor: pointer;">
<!-- splitChars() dynamically generates the spans:
<span class="char" data-char="Y">?</span>
<span class="char" data-char="O">?</span>
… one span per character in data-text -->
</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 |
|---|---|---|
| data-text | "SHUFFLE" | Text to reveal. Updatable on the fly: set the attribute, call splitChars(el), then shuffleReveal(el). |
| CHARS (JS const) | A-Za-z0-9 + symbols | Random character pool. Narrow to 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' for a clean typographic look; add digits and symbols for the hacker aesthetic. |
| CYCLE_SPEED (JS const) | 50 ms | Interval in ms between substitutions per position. 35 ms = very fast (hacker), 80–100 ms = slow and editorial. |
| CYCLES_PER_CHAR (JS const) | 8 | Number of random substitutions before locking. Raise (12–16) to extend tension; lower (4–5) for a near-instant reveal. |
| STAGGER (JS const) | 60 ms | Delay in ms between each position's start. 0 = all letters cycle simultaneously; 120 ms = very progressive cascade. |
| .kt-text { font-size } | 2.2rem | Text size — line-height and letter-spacing inherit automatically. |
| transitional color (inline) | rgba(255,255,255,0.5) | Color of in-transit random chars. The locked value snaps to #fff (full opacity) at the end of the cycle. |
FAQ
data-text attribute, call splitChars(el) to rebuild the spans, then restart the animation: el.setAttribute('data-text','NEW'); splitChars(el); shuffleReveal(el);click listener with an IntersectionObserver on the container. In the callback, call shuffleReveal(el) when entry.isIntersecting is true, then observer.disconnect() if you only want a single trigger.shuffleReveal inside a useEffect after mount; in Vue, inside the mounted hook; in Svelte, inside onMount. The animation only touches textContent and style.color of the spans — no conflicts with framework rendering.