Spy films love the moment a password cracks letter by letter. This text field brings that moment to typing: each new character arrives as a blur of random symbols and settles into the real letter a fraction of a second later. The field underneath stays a completely normal input.
How it works
The trick is two layers in the same box. The real <input> sits on top with transparent text, so it still handles typing, selection, paste and the caret. Underneath, a plain div shows what the reader sees. Both use the same monospace font, size and padding, so every letter lines up with the caret.
- Compare. On each input event the script walks the new value letter by letter. Letters that did not change keep their place. New or changed letters get a short timer, between about a quarter and two thirds of a second.
- Scramble. While a letter's timer runs, each frame shows a random symbol from a small alphabet, highlighted in gold.
- Settle. When the timer ends, the real letter appears. Once every letter has settled the animation loop stops completely.
- Respect other keyboards. Chinese, Japanese and Korean input methods compose a character in several steps. During composition the script shows the raw text and only animates the finished character, so the effect never fights the input method.
The code
<div class="dc" data-lab="decode-input">
<label for="dc-in">Codename</label>
<div class="dc-field">
<input id="dc-in" type="text" maxlength="22" autocomplete="off" spellcheck="false" placeholder="Type something">
<div class="dc-view" aria-hidden="true"></div>
</div>
</div>.dc {
display: grid;
gap: 10px;
width: min(100%, 440px);
font-family: system-ui, sans-serif;
}
.dc label { font-size: 13px; letter-spacing: .12em; text-transform: uppercase; color: #46534a; }
.dc-field { position: relative; background: #101a14; border-radius: 12px; }
/* The real input and the animated copy share one font and box, so the letters line up. */
.dc-field input,
.dc-view {
box-sizing: border-box;
width: 100%;
height: 60px;
padding: 0 18px;
font: 500 22px/60px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
letter-spacing: .06em;
white-space: pre;
}
.dc-field input {
position: relative;
z-index: 2;
border: 0;
border-radius: 12px;
background: transparent;
color: transparent;
caret-color: #f4d96c;
outline: none;
}
.dc-field input::placeholder { color: rgba(231, 235, 227, .35); }
.dc-field input:focus-visible { box-shadow: 0 0 0 2px #f4d96c; }
.dc-view {
position: absolute;
inset: 0;
z-index: 1;
color: #e7ebe3;
pointer-events: none;
overflow: hidden;
}
.dc-view .s { color: #f4d96c; opacity: .85; }(function () {
var root = document.querySelector('[data-lab="decode-input"]');
if (!root) return;
var input = root.querySelector('input');
var view = root.querySelector('.dc-view');
var GLYPHS = 'ABCDEFGHJKLMNPQRSTUVWXYZ0123456789#%&*+=?@$';
var reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
var chars = [], composing = false, raf = 0;
function randomGlyph() { return GLYPHS[Math.floor(Math.random() * GLYPHS.length)]; }
function esc(s) { return s.replace(/&/g, '&').replace(/</g, '<'); }
// Compare the new value with what is on screen. Unchanged letters stay put,
// new or changed letters get a short scramble before they settle.
function sync() {
var val = input.value, now = performance.now(), next = [];
for (var i = 0; i < val.length; i++) {
var old = chars[i];
if (old && old.ch === val[i]) next.push(old);
else next.push({ ch: val[i], until: reduce || val[i] === ' ' ? 0 : now + 260 + Math.random() * 380, shown: randomGlyph() });
}
chars = next;
if (!raf) raf = requestAnimationFrame(tick);
}
function tick(t) {
var busy = false, html = '';
chars.forEach(function (c) {
if (t < c.until) {
busy = true;
if (Math.random() < 0.5) c.shown = randomGlyph();
html += '<span class="s">' + esc(c.shown) + '</span>';
} else {
html += esc(c.ch);
}
});
view.innerHTML = html;
view.scrollLeft = input.scrollLeft;
raf = busy ? requestAnimationFrame(tick) : 0;
}
// Chinese, Japanese and Korean keyboards compose a character in several steps.
// Show the raw text while composing and only animate the finished character.
input.addEventListener('compositionstart', function () { composing = true; });
input.addEventListener('compositionend', function () { composing = false; sync(); });
input.addEventListener('input', function () {
if (composing) { view.textContent = input.value; return; }
sync();
});
input.addEventListener('scroll', function () { view.scrollLeft = input.scrollLeft; });
})();When to use it, and when not to
It fits a codename or username field on a game, an event microsite, a coming soon page or a tech themed landing page, anywhere the input is short and the mood is playful.
Skip it for long text, passwords and anything people need to proofread: a scrambling letter is harder to check than a still one. It also adds nothing for screen reader users, which is fine, because the animated layer is hidden from them with aria-hidden and they get the ordinary input. People who ask for reduced motion see letters appear instantly.
Make it yours
- The symbols: change
GLYPHS. Katakana or binary digits give very different moods. - Duration: the
260 + Math.random() * 380range sets how long each letter scrambles, in milliseconds. - Length: the demo caps input at 22 characters with
maxlength, which keeps both layers aligned without horizontal scrolling.
Frequently asked questions
Why does it need a monospace font?
Every character in a monospace font has the same width, so the visible layer lines up with the invisible input and the caret lands in the right place. With a proportional font, random symbols would be wider or narrower than the real letters.
Does copy and paste still work?
Yes. Readers interact with the real input the whole time; only the display layer is animated.
Will form autofill break it?
Autofill fires an input event like typing does, so the filled text decodes the same way. The demo turns autocomplete off only to keep the example tidy.


