Password Checklist
The password rules under the field tick themselves off as you type. Each circle fills with colour and a tick draws in with a little pop. Break a rule again and it unticks.
How to use it
- Paste the snippet just before
</head>. - Wrap a password input in an element with
data-password-checklist. - The list of rules is added under the field.
Options
Add any of these attributes to the same element.
data-rules | Which rules, in order, comma-separated from length, upper, lower, number and symbol. Default length,upper,number,symbol. |
data-min | The minimum length. Default 8. |
data-color | Tick colour. Default #16a34a. |
data-autoplay | It types a password that meets the rules one by one, then deletes it, over and over while in view. It stops as soon as you use the field. |
data-text | What autoplay types. Default Sunset42!. |
Listen for the checklist event: e.detail.met is true once every rule passes.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Password Checklist: a list of rules under the password field ticks
// itself off as you type. Each rule's circle fills with colour and a tick
// draws in with a little pop; break the rule again and it unticks.
// Usage: <div data-password-checklist><input type="password"></div>
// The list is added inside the wrapper, under the field.
// Optional: data-min (minimum length, default 8), data-rules (which rules
// and in what order, comma-separated from: length, upper, lower, number,
// symbol; default "length,upper,number,symbol"), data-color (tick colour,
// default #16a34a), data-autoplay (it types a password that meets the rules
// one by one, then deletes it, over and over while in view; it stops as
// soon as you use the field), data-text (what autoplay types, default
// "Sunset42!").
// Listen for the "checklist" event: e.detail.met is true when every rule passes.
var DEFAULTS = { min: 8, rules: 'length,upper,number,symbol', color: '#16a34a', text: 'Sunset42!' };
var RULES = {
length: { text: function (min) { return 'At least ' + min + ' characters'; }, test: function (p, min) { return p.length >= min; } },
upper: { text: function () { return 'One uppercase letter'; }, test: function (p) { return /[A-Z]/.test(p); } },
lower: { text: function () { return 'One lowercase letter'; }, test: function (p) { return /[a-z]/.test(p); } },
number: { text: function () { return 'One number'; }, test: function (p) { return /\d/.test(p); } },
symbol: { text: function () { return 'One symbol'; }, test: function (p) { return /[^A-Za-z0-9\s]/.test(p); } }
};
var css =
'.pcl-list{display:grid;gap:0.45em;margin:0.8em 0 0;padding:0;list-style:none;text-align:left;font-size:0.88em}' +
'.pcl-item{display:flex;align-items:center;gap:0.55em;color:#71717a;transition:color .25s}' +
'.pcl-met{color:inherit}' +
'.pcl-dot{position:relative;flex:none;display:grid;place-items:center;width:1.2em;height:1.2em;border-radius:50%;box-sizing:border-box;border:0.1em solid #d4d4d8}' +
'.pcl-fill{position:absolute;inset:-0.1em;border-radius:50%;background:var(--pcl-color);transform:scale(0)}' +
'.pcl-dot svg{position:relative;width:70%;height:70%;overflow:visible}' +
'.pcl-sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}' +
'.pcl-text{white-space:pre;letter-spacing:0.08em}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
function on(v) { return v !== undefined && v !== 'false'; }
function setup(el) {
var d = el.dataset;
var input = el.querySelector('input');
var field = input || el.querySelector('[data-field]') || el.firstElementChild;
if (!field) return;
var real = !!input;
var min = parseInt(d.min, 10) || DEFAULTS.min;
el.style.setProperty('--pcl-color', d.color || DEFAULTS.color);
var list = document.createElement('ul');
list.className = 'pcl-list';
var items = (d.rules || DEFAULTS.rules).split(',').map(function (k) {
var rule = RULES[k.trim()];
if (!rule) return null;
var li = document.createElement('li');
li.className = 'pcl-item';
li.innerHTML = '<span class="pcl-dot" aria-hidden="true"><span class="pcl-fill"></span>' +
'<svg viewBox="0 0 24 24"><path d="M5 12.5l4.5 4.5L19 7.5" fill="none" stroke="#fff" stroke-width="3.4" stroke-linecap="round" stroke-linejoin="round"/></svg></span>' +
'<span></span><span class="pcl-sr"></span>';
li.children[1].textContent = rule.text(min);
list.appendChild(li);
return {
rule: rule, li: li, dot: li.firstChild, fill: li.firstChild.firstChild, sr: li.lastChild,
tick: anime.svg.createDrawable(li.querySelector('path')), met: null
};
}).filter(Boolean);
el.appendChild(list);
var text = null, value = '';
if (!real) {
text = document.createElement('span');
text.className = 'pcl-text';
field.textContent = '';
field.appendChild(text);
}
var get = function () { return real ? input.value : value; };
var set = function (v) {
if (real) input.value = v;
else { value = v; text.textContent = v.replace(/./g, '•'); }
update();
};
var allMet = null;
function update() {
var p = get();
var delay = 0;
items.forEach(function (it) {
var met = it.rule.test(p, min);
if (met === it.met) return;
var first = it.met === null;
it.met = met;
it.li.classList.toggle('pcl-met', met);
it.sr.textContent = met ? ' (done)' : ' (not yet)';
if (first || reduced()) {
anime.utils.set(it.fill, { scale: met ? 1 : 0 });
anime.utils.set(it.tick, { draw: met ? '0 1' : '0 0' });
return;
}
if (met) {
// The dot squeezes, fills from the middle, and the tick draws in.
anime.animate(it.dot, { scale: [0.75, 1], duration: 500, delay: delay, ease: 'outElastic(1, .5)' });
anime.animate(it.fill, { scale: [0, 1], duration: 300, delay: delay, ease: 'out(3)' });
anime.animate(it.tick, { draw: ['0 0', '0 1'], duration: 320, delay: delay + 140, ease: 'out(3)' });
anime.animate(it.li.children[1], { x: [0, '0.25em', 0], duration: 360, delay: delay + 80, ease: 'out(2)' });
delay += 90;
} else {
anime.animate(it.tick, { draw: '1 1', duration: 180, ease: 'in(2)' });
anime.animate(it.fill, { scale: 0, duration: 240, delay: 120, ease: 'in(2)' });
}
});
var all = items.every(function (it) { return it.met; });
if (all !== allMet) {
allMet = all;
el.dispatchEvent(new CustomEvent('checklist', { bubbles: true, detail: { met: all } }));
}
}
if (real) {
input.addEventListener('input', update);
input.addEventListener('pointerdown', stop);
input.addEventListener('keydown', stop);
input.addEventListener('focus', stop);
}
update();
var timer = null, stopped = false;
function stop() {
if (stopped) return;
stopped = true;
clearTimeout(timer);
}
if (on(d.autoplay) && !reduced()) {
var pw = d.text || DEFAULTS.text;
var steps = [];
var i;
for (i = 1; i <= pw.length; i++) {
(function (n) { steps.push([function () { set(pw.slice(0, n)); }, 220 + Math.random() * 120]); })(i);
}
steps.push([function () {}, 1700]);
for (i = pw.length - 1; i >= 0; i--) {
(function (n) { steps.push([function () { set(pw.slice(0, n)); }, 55]); })(i);
}
steps.push([function () {}, 900]);
var run = function (i) {
if (stopped) return;
steps[i][0]();
timer = setTimeout(function () { run((i + 1) % steps.length); }, steps[i][1]);
};
new IntersectionObserver(function (entries) {
clearTimeout(timer);
if (stopped) return;
if (entries[0].isIntersecting) { set(''); timer = setTimeout(function () { run(0); }, 600); }
}).observe(el);
}
}
function init() {
document.querySelectorAll('[data-password-checklist]').forEach(function (el) {
if (el._pcl) return;
el._pcl = true;
setup(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><div data-password-checklist data-min="10">
<input type="password" aria-label="Password">
</div>