25 CSS Text Animations 18 / 25
CSS Word Scramble Animation
Characters rapidly cycle through random glyphs before locking into the final letter — a hacker/terminal decryption effect powered by JavaScript.
The code
<div class="ta-18">
<div class="ta-18__stage">
<p class="ta-18__label">Decrypting</p>
<h2 class="ta-18__text" id="ta-18-text" aria-label="ACCESS GRANTED"></h2>
<p class="ta-18__sub">JS character cycling · random glyph pool · staggered resolve</p>
</div>
</div> <div class="ta-18">
<div class="ta-18__stage">
<p class="ta-18__label">Decrypting</p>
<h2 class="ta-18__text" id="ta-18-text" aria-label="ACCESS GRANTED"></h2>
<p class="ta-18__sub">JS character cycling · random glyph pool · staggered resolve</p>
</div>
</div>.ta-18, .ta-18 *, .ta-18 *::before, .ta-18 *::after {
margin: 0; padding: 0; box-sizing: border-box;
}
.ta-18 ::selection { background: #065f46; color: #d1fae5; }
.ta-18 {
--bg: #030d06;
--green: #4ade80;
--dim: #166534;
min-height: 100vh;
background: var(--bg);
display: flex;
align-items: center;
justify-content: center;
padding: 2rem;
font-family: 'JetBrains Mono', 'Courier New', monospace;
}
.ta-18__stage { text-align: center; }
.ta-18__label {
font-size: 0.68rem;
letter-spacing: 0.25em;
text-transform: uppercase;
color: var(--dim);
margin-bottom: 0.5rem;
}
.ta-18__text {
font-size: clamp(1.8rem, 5.5vw, 3.2rem);
font-weight: 700;
letter-spacing: 0.1em;
display: flex;
justify-content: center;
flex-wrap: wrap;
gap: 0.05em;
min-height: 1.2em;
}
.ta-18__char {
display: inline-block;
color: var(--dim);
transition: color 0.1s;
}
.ta-18__char.resolved {
color: var(--green);
text-shadow: 0 0 8px rgba(74, 222, 128, 0.5);
}
.ta-18__sub {
font-size: 0.65rem;
color: #052010;
margin-top: 0.8rem;
letter-spacing: 0.08em;
}
@media (prefers-reduced-motion: reduce) {
.ta-18__char { transition: none; }
} .ta-18, .ta-18 *, .ta-18 *::before, .ta-18 *::after {
margin: 0; padding: 0; box-sizing: border-box;
}
.ta-18 ::selection { background: #065f46; color: #d1fae5; }
.ta-18 {
--bg: #030d06;
--green: #4ade80;
--dim: #166534;
min-height: 100vh;
background: var(--bg);
display: flex;
align-items: center;
justify-content: center;
padding: 2rem;
font-family: 'JetBrains Mono', 'Courier New', monospace;
}
.ta-18__stage { text-align: center; }
.ta-18__label {
font-size: 0.68rem;
letter-spacing: 0.25em;
text-transform: uppercase;
color: var(--dim);
margin-bottom: 0.5rem;
}
.ta-18__text {
font-size: clamp(1.8rem, 5.5vw, 3.2rem);
font-weight: 700;
letter-spacing: 0.1em;
display: flex;
justify-content: center;
flex-wrap: wrap;
gap: 0.05em;
min-height: 1.2em;
}
.ta-18__char {
display: inline-block;
color: var(--dim);
transition: color 0.1s;
}
.ta-18__char.resolved {
color: var(--green);
text-shadow: 0 0 8px rgba(74, 222, 128, 0.5);
}
.ta-18__sub {
font-size: 0.65rem;
color: #052010;
margin-top: 0.8rem;
letter-spacing: 0.08em;
}
@media (prefers-reduced-motion: reduce) {
.ta-18__char { transition: none; }
}(function() {
const el = document.getElementById('ta-18-text');
if (!el) return;
const finalText = 'ACCESS GRANTED';
const chars = '!@#$%^&*<>?/|\\[]{}ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
const resolveDelay = 6;
function scramble() {
el.innerHTML = '';
const spans = [];
[...finalText].forEach((ch) => {
const span = document.createElement('span');
span.className = 'ta-18__char';
span.textContent = ch === ' ' ? ' ' : chars[Math.floor(Math.random() * chars.length)];
el.appendChild(span);
spans.push({ el: span, final: ch, cycles: 0, resolved: false });
});
let tick = 0;
const id = setInterval(() => {
tick++;
let allDone = true;
spans.forEach((s, i) => {
if (s.resolved) return;
if (tick >= resolveDelay + i * 3) {
s.el.textContent = s.final === ' ' ? ' ' : s.final;
s.el.classList.add('resolved');
s.resolved = true;
} else {
s.el.textContent = s.final === ' ' ? ' ' : chars[Math.floor(Math.random() * chars.length)];
allDone = false;
}
});
if (allDone) {
clearInterval(id);
setTimeout(scramble, 2200);
}
}, 65);
}
scramble();
})(); (function() {
const el = document.getElementById('ta-18-text');
if (!el) return;
const finalText = 'ACCESS GRANTED';
const chars = '!@#$%^&*<>?/|\\[]{}ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
const resolveDelay = 6;
function scramble() {
el.innerHTML = '';
const spans = [];
[...finalText].forEach((ch) => {
const span = document.createElement('span');
span.className = 'ta-18__char';
span.textContent = ch === ' ' ? ' ' : chars[Math.floor(Math.random() * chars.length)];
el.appendChild(span);
spans.push({ el: span, final: ch, cycles: 0, resolved: false });
});
let tick = 0;
const id = setInterval(() => {
tick++;
let allDone = true;
spans.forEach((s, i) => {
if (s.resolved) return;
if (tick >= resolveDelay + i * 3) {
s.el.textContent = s.final === ' ' ? ' ' : s.final;
s.el.classList.add('resolved');
s.resolved = true;
} else {
s.el.textContent = s.final === ' ' ? ' ' : chars[Math.floor(Math.random() * chars.length)];
allDone = false;
}
});
if (allDone) {
clearInterval(id);
setTimeout(scramble, 2200);
}
}, 65);
}
scramble();
})();How this works
JavaScript splits the target word into individual characters and uses setInterval to rapidly replace each character's displayed glyph with a random character from a pool of symbols, digits, and letters. The interval fires every 50–80ms, creating a flickering scramble. Simultaneously, a counter tracks how many 'resolve' cycles each character has completed before locking it to its final value.
Characters resolve left-to-right: position 0 locks first after N cycles, position 1 locks after N+k cycles, and so on. The CSS styles each character span with a monospace font and a distinct colour — scrambling characters shown in a secondary accent colour while resolved characters display in the final primary colour, providing clear visual feedback about the decode progress.
Customize
- Change the character pool in the
charsstring — use only digits for a numeric ticker, only symbols for a sci-fi alien script, or Greek letters for a scholarly look. - Adjust scramble speed by changing the
setIntervaldelay from60ms — faster (30ms) for a frantic glitch, slower (120ms) for a deliberate decode. - Change the stagger rate by adjusting the
resolveAftercalculation per character — a smaller multiplier resolves all characters almost simultaneously. - Trigger on hover using a
mouseenterevent listener that starts the scramble, combined with amouseleavethat resets to the original text. - Chain multiple elements by starting the next word's scramble when the previous resolves, creating a cascading reveal of a headline, subheading, and body line.
Watch out for
- Always clear the interval with
clearIntervalwhen all characters have resolved — leaking intervals cause the effect to restart unexpectedly and waste CPU. - Screen readers will read out every scrambled character update as it changes — always add
aria-live="off"andaria-labelwith the final text on the container. - Rapidly updating DOM text nodes causes reflows if the element width changes character-by-character — use a monospaced font and set a fixed
min-widthon the container equal to the final word width.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| All | All | All | All |
Pure DOM manipulation with setInterval — works in every browser that supports JavaScript. Ensure aria-label provides the final readable text.