← Typing

Form Autofill

A form fills itself in like a password manager at work. A saved account drops down under the first field and is picked, then each field fills in turn with a quick tinted sweep and a key icon, the password as dots, before the button is pressed.

Alex Morgan

[email protected]

correct-horse-9

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-form-autofill to a container with one element per field. Give each a data-label; its text is what fills in.

Options

Add any of these attributes to the same element.

data-type="password"On a field: show its value as dots.
data-accountThe saved account shown in the suggestion. Default the first field’s value.
data-buttonThe button’s text. Default “Sign in”.
data-speedTime per character as a field fills. Default 26 ms.
data-loopClear the form and fill it again after a pause.
data-holdHow long the filled form stays before looping. Default 2200 ms.

It is a picture of a form, not a working one: the element is labelled with the field names and values for screen readers, with passwords left out.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Form Autofill: a sign-in form fills itself in like a password manager at
    // work. A saved-account suggestion drops down under the first field, is
    // picked, and the fields fill one by one with a tinted flash, the password
    // as dots, before the button is pressed.
    // Usage:
    //   <div data-form-autofill>
    //     <p data-label="Email">[email protected]</p>
    //     <p data-label="Password" data-type="password">correct-horse</p>
    //   </div>
    // One element per field: data-label is its label, the text is what fills in,
    // and data-type="password" shows it as dots.
    // Optional: data-account (the suggestion's text, default the first value),
    // data-button (button text, default "Sign in"), data-speed (ms per
    // character, default 26), data-loop (clear and fill again), data-hold (ms the
    // filled form stays before looping, default 2200).
    var DEFAULTS = { button: 'Sign in', speed: 26, hold: 2200 };

    var KEY = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">' +
      '<circle cx="7.5" cy="15.5" r="4.5"/><path d="m10.7 12.3 9.3-9.3M16 7l3 3M14 9l2 2"/></svg>';

    var css =
      '.faf{position:relative;text-align:left;background:#fff;color:#1f2328;border-radius:14px;border:1px solid rgba(0,0,0,.08);' +
      'box-shadow:0 20px 44px -16px rgba(40,10,70,.3);padding:1.1em 1.2em 1.2em}' +
      '.faf-field{position:relative;margin:0 0 0.8em}' +
      '.faf-lab{display:block;font-size:0.82em;font-weight:600;color:#6b7280;margin:0 0 0.35em}' +
      '.faf-box{position:relative;display:flex;align-items:center;gap:0.5em;height:2.4em;padding:0 0.8em;border-radius:8px;border:1.5px solid #e2e4e9;' +
      'background:#fff;white-space:nowrap;overflow:hidden;transition:border-color .25s,background-color .5s}' +
      '.faf-box.faf-on{border-color:var(--accent,#6a0dad);box-shadow:0 0 0 3px var(--accent-soft,#f3e8ff)}' +
      '.faf-box.faf-filled{background:#eef4ff}' +
      '.faf-txt{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis}' +
      '.faf-pw{letter-spacing:0.12em}' +
      '.faf-key{flex:none;width:1.1em;height:1.1em;color:var(--accent,#6a0dad);opacity:0}' +
      '.faf-key svg{display:block;width:100%;height:100%}' +
      '.faf-flash{position:absolute;inset:0;background:linear-gradient(90deg,transparent,rgba(106,13,173,.18),transparent);opacity:0;pointer-events:none}' +
      '.faf-menu{position:absolute;z-index:2;left:0;right:0;top:100%;margin-top:0.35em;background:#fff;border-radius:10px;border:1px solid rgba(0,0,0,.08);' +
      'box-shadow:0 14px 32px -8px rgba(30,10,60,.35);padding:0.35em;opacity:0;visibility:hidden;transform-origin:50% 0}' +
      '.faf-opt{display:flex;align-items:center;gap:0.6em;padding:0.45em 0.6em;border-radius:7px;white-space:nowrap;overflow:hidden}' +
      '.faf-opt.faf-hot{background:var(--accent-soft,#f3e8ff)}' +
      '.faf-ava{flex:none;display:grid;place-items:center;width:1.8em;height:1.8em;border-radius:50%;background:var(--accent,#6a0dad);color:#fff}' +
      '.faf-ava svg{width:1em;height:1em}' +
      '.faf-opt span{min-width:0;overflow:hidden;text-overflow:ellipsis}' +
      '.faf-opt small{display:block;font-size:0.78em;color:#6b7280}' +
      '.faf-btn{display:flex;align-items:center;justify-content:center;gap:0.5em;height:2.5em;margin-top:1.1em;border-radius:8px;' +
      'background:var(--accent,#6a0dad);color:#fff;font-weight:600}' +
      '.faf-btn b{display:inline-grid;place-items:center;width:1.3em;height:1.3em;border-radius:50%;background:#22c55e;font-size:0.8em;opacity:0;width:0;overflow:hidden}' +
      '.faf-caret{display:inline-block;width:2px;height:1.1em;margin-left:1px;vertical-align:-0.2em;background:var(--accent,#6a0dad)}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

    function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
    function sleep(ms) { return new Promise(function (r) { setTimeout(r, ms); }); }
    function opt(d, k) { return d[k] !== undefined && d[k] !== '' ? d[k] : DEFAULTS[k]; }
    function dots(n) { return new Array(n + 1).join('•'); }

    function setup(el) {
      var d = el.dataset;
      var fields = Array.from(el.children).map(function (c) {
        return { label: c.getAttribute('data-label') || '', value: c.textContent.trim(), pw: c.getAttribute('data-type') === 'password' };
      });
      el.setAttribute('role', 'img');
      el.setAttribute('aria-label', 'A form filling itself in: ' + fields.map(function (f) {
        return f.label + (f.pw ? ' (hidden)' : ': ' + f.value);
      }).join(', '));
      el.textContent = '';
      el.classList.add('faf');
      var inner = document.createElement('div');
      inner.setAttribute('aria-hidden', 'true');
      fields.forEach(function (f, i) {
        var w = document.createElement('div');
        w.className = 'faf-field';
        w.innerHTML = '<span class="faf-lab"></span><div class="faf-box"><span class="faf-txt' + (f.pw ? ' faf-pw' : '') + '"></span>' +
          '<span class="faf-key">' + KEY + '</span><span class="faf-flash"></span></div>';
        w.firstChild.textContent = f.label;
        f.box = w.querySelector('.faf-box');
        f.txt = w.querySelector('.faf-txt');
        f.key = w.querySelector('.faf-key');
        f.flash = w.querySelector('.faf-flash');
        if (i === 0) {
          var menu = document.createElement('div');
          menu.className = 'faf-menu';
          menu.innerHTML = '<div class="faf-opt"><span class="faf-ava">' + KEY + '</span><span><b></b><small>Saved account</small></span></div>';
          menu.querySelector('b').textContent = opt(d, 'account') || f.value;
          w.appendChild(menu);
          f.menu = menu;
        }
        inner.appendChild(w);
      });
      var btn = document.createElement('div');
      btn.className = 'faf-btn';
      btn.innerHTML = '<span></span><b>✓</b>';
      btn.firstChild.textContent = opt(d, 'button');
      inner.appendChild(btn);
      el.appendChild(inner);
      var caret = document.createElement('span');
      caret.className = 'faf-caret';
      return { el: el, fields: fields, btn: btn, caret: caret, visible: false, wake: null };
    }

    function show(f, text) { f.txt.textContent = f.pw ? dots(text.length) : text; }

    function clear(s) {
      s.fields.forEach(function (f) {
        f.txt.textContent = '';
        f.box.classList.remove('faf-filled', 'faf-on');
        anime.utils.set(f.key, { opacity: 0 });
      });
      var tick = s.btn.querySelector('b');
      anime.utils.set(tick, { opacity: 0, width: 0 });
    }

    function finished(s) {
      s.fields.forEach(function (f) {
        show(f, f.value);
        f.box.classList.add('faf-filled');
        anime.utils.set(f.key, { opacity: 1 });
      });
    }

    function whenVisible(s) {
      return s.visible ? Promise.resolve() : new Promise(function (r) { s.wake = r; });
    }

    function fill(s, f, speed) {
      f.box.classList.add('faf-on');
      f.txt.textContent = '';
      f.box.insertBefore(s.caret, f.key);
      anime.animate(f.flash, { opacity: [0, 1, 0], translateX: ['-60%', '60%'], duration: 650, ease: 'inOut(2)' });
      anime.animate(f.key, { opacity: [0, 1], scale: [0.4, 1], duration: 380, ease: 'outBack(2)' });
      f.box.classList.add('faf-filled');
      return new Promise(function (done) {
        var n = 0;
        (function tick() {
          show(f, f.value.slice(0, n));
          if (n++ >= f.value.length) { done(); return; }
          setTimeout(tick, speed);
        })();
      }).then(function () {
        return sleep(160);
      }).then(function () {
        f.box.classList.remove('faf-on');
        s.caret.remove();
      });
    }

    function play(s) {
      var d = s.el.dataset;
      var speed = +opt(d, 'speed');
      var first = s.fields[0];
      var hot = first.menu.querySelector('.faf-opt');
      clear(s);
      var chain = sleep(500).then(function () {
        first.box.classList.add('faf-on');
        first.menu.style.visibility = 'visible';
        anime.animate(first.menu, { opacity: [0, 1], y: [-6, 0], scale: [0.96, 1], duration: 320, ease: 'out(3)' });
        return sleep(700);
      }).then(function () {
        hot.classList.add('faf-hot');
        return sleep(380);
      }).then(function () {
        anime.animate(hot, { scale: [1, 0.96, 1], duration: 240, ease: 'inOut(2)' });
        return sleep(240);
      }).then(function () {
        anime.animate(first.menu, {
          opacity: 0, y: -4, duration: 200, ease: 'in(2)',
          onComplete: function () { first.menu.style.visibility = 'hidden'; hot.classList.remove('faf-hot'); }
        });
        return sleep(120);
      });
      s.fields.forEach(function (f) {
        chain = chain.then(function () { return fill(s, f, speed); }).then(function () { return sleep(90); });
      });
      chain.then(function () {
        return sleep(350);
      }).then(function () {
        anime.animate(s.btn, { scale: [1, 0.95, 1], duration: 260, ease: 'inOut(2)' });
        anime.animate(s.btn.querySelector('b'), { opacity: [0, 1], width: ['0em', '1.3em'], duration: 380, delay: 160, ease: 'outBack(2)' });
        if (d.loop === undefined || d.loop === 'false') return;
        return sleep(+opt(d, 'hold')).then(function () { return whenVisible(s); }).then(function () {
          anime.animate(s.fields.map(function (f) { return f.txt; }), { opacity: [1, 0], duration: 300, ease: 'in(2)' });
          return sleep(320);
        }).then(function () {
          s.fields.forEach(function (f) { anime.utils.set(f.txt, { opacity: 1 }); });
          play(s);
        });
      });
    }

    function init() {
      var io = new IntersectionObserver(function (entries) {
        entries.forEach(function (e) {
          var s = e.target._faf;
          s.visible = e.isIntersecting;
          if (s.visible && s.wake) { var w = s.wake; s.wake = null; w(); }
        });
      }, { threshold: 0.3 });
      document.querySelectorAll('[data-form-autofill]').forEach(function (el) {
        if (el._faf) return;
        var s = (el._faf = setup(el));
        if (reduced()) { finished(s); return; }
        clear(s);
        io.observe(el);
        whenVisible(s).then(function () { play(s); });
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<div data-form-autofill data-loop>
  <p data-label="Email">[email protected]</p>
  <p data-label="Password" data-type="password">correct-horse</p>
</div>