Decoding text input: a scramble effect as you type

Every letter arrives as a flicker of random symbols and settles a moment later, while the real input underneath works as usual.

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.

Live demo. It runs on exactly the code further down this page.

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.

  1. 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.
  2. Scramble. While a letter's timer runs, each frame shows a random symbol from a small alphabet, highlighted in gold.
  3. Settle. When the timer ends, the real letter appears. Once every letter has settled the animation loop stops completely.
  4. 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

HTML
<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>
CSS
.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; }
JavaScript
(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, '&amp;').replace(/</g, '&lt;'); }

  // 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() * 380 range 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.

What should your website do?

Describe a feature you wish your site had. If it is useful to other owners too, we build it here as a free Lab post with a live demo and code.

We never publish your name, email or website. Privacy

More from the Website Lab

All experiments