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.
How to use it
- Paste the snippet just before
</head>. - Add
data-form-autofillto a container with one element per field. Give each adata-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-account | The saved account shown in the suggestion. Default the first field’s value. |
data-button | The button’s text. Default “Sign in”. |
data-speed | Time per character as a field fills. Default 26 ms. |
data-loop | Clear the form and fill it again after a pause. |
data-hold | How 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.
<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><div data-form-autofill data-loop>
<p data-label="Email">[email protected]</p>
<p data-label="Password" data-type="password">correct-horse</p>
</div>