Password Strength Meter
A bar under the password field fills as the password gets stronger, sliding from red through orange and amber to green, while a word rolls over at each step. It counts length, mixed case, numbers and symbols.
How to use it
- Paste the snippet just before
</head>. - Wrap a password input in an element with
data-password-strength-meter. - The meter is added under the field.
Options
Add any of these attributes to the same element.
data-labels | The four words, comma-separated. Default Weak,Fair,Good,Strong. |
data-colors | The four colours, comma-separated. Default red, orange, amber, green. |
data-autoplay | It types a password that gets stronger, 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!Blue. |
Listen for the strength event: e.detail.level runs from 0 (empty) to 4. animacious.passwordStrength(text) gives the same score from code. It’s a nudge, not a security check: do the real rules on your server.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Password Strength Meter: a bar under the password field fills as the
// password gets stronger, sliding from red through orange and amber to
// green, with a word (Weak, Fair, Good, Strong) that rolls over at each
// step. Strength counts length (8+, 12+), mixed case, digits and symbols.
// Usage: <div data-password-strength-meter><input type="password"></div>
// The meter is added inside the wrapper, under the field.
// Optional: data-labels (four words, comma-separated, default
// "Weak,Fair,Good,Strong"), data-colors (four colours, comma-separated),
// data-autoplay (it types a password that gets stronger, 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!Blue").
// Listen for the "strength" event: e.detail.level is 0 (empty) to 4.
// Script use: animacious.passwordStrength("text") returns that level.
var DEFAULTS = {
labels: 'Weak,Fair,Good,Strong',
colors: '#dc2626,#f97316,#eab308,#16a34a',
text: 'Sunset42!Blue'
};
var css =
'.psm-meter{display:flex;align-items:center;gap:0.7em;margin-top:0.6em}' +
'.psm-track{position:relative;flex:1;height:0.38em;border-radius:99px;background:rgba(0,0,0,0.08);overflow:hidden}' +
'.psm-fill{position:absolute;left:0;top:0;bottom:0;width:0;border-radius:inherit}' +
'.psm-word{position:relative;flex:none;width:4.4em;height:1.3em;overflow:hidden;font-size:0.82em;font-weight:600;line-height:1.3em;text-align:right}' +
'.psm-word span{position:absolute;right:0;top:0;white-space:nowrap}' +
'.psm-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 strength(p) {
if (!p) return 0;
var s = 0;
if (p.length >= 8) s++;
if (p.length >= 12) s++;
if (/[a-z]/.test(p) && /[A-Z]/.test(p)) s++;
if (/\d/.test(p)) s++;
if (/[^A-Za-z0-9]/.test(p)) s++;
if (p.length < 8) s = Math.min(s, 2);
return Math.max(1, Math.min(4, s));
}
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 labels = (d.labels || DEFAULTS.labels).split(',');
var colors = (d.colors || DEFAULTS.colors).split(',');
var meter = document.createElement('div');
meter.className = 'psm-meter';
meter.innerHTML = '<span class="psm-track" aria-hidden="true"><span class="psm-fill"></span></span><span class="psm-word" aria-live="polite"></span>';
el.appendChild(meter);
var track = meter.firstChild, fill = track.firstChild, word = meter.lastChild;
var text = null, value = '';
if (!real) {
text = document.createElement('span');
text.className = 'psm-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 level = -1;
function update() {
var next = strength(get());
if (next === level) return;
var up = next > level;
level = next;
var c = colors[Math.max(0, next - 1)];
var w = next * 25 + '%';
el.dispatchEvent(new CustomEvent('strength', { bubbles: true, detail: { level: next } }));
// The word rolls over: the old one leaves, the new one arrives.
// Anything still leaving from a quicker change goes at once.
Array.from(word.children).forEach(function (c) { if (c._psmOut) c.remove(); });
var old = word.firstChild;
if (old) old._psmOut = true;
var neu = document.createElement('span');
neu.textContent = next ? labels[next - 1] : '';
neu.style.color = c;
word.appendChild(neu);
if (reduced()) {
if (old) old.remove();
fill.style.width = w;
fill.style.backgroundColor = c;
return;
}
if (old) {
anime.animate(old, { y: up ? '-100%' : '100%', opacity: 0, duration: 260, ease: 'in(2)' });
setTimeout(function () { old.remove(); }, 280);
}
anime.animate(neu, { y: [up ? '100%' : '-100%', '0%'], opacity: [0, 1], duration: 380, delay: 60, ease: 'out(3)' });
anime.animate(fill, { width: w, backgroundColor: c, duration: up ? 700 : 400, ease: up ? 'outElastic(1, .75)' : 'out(3)' });
if (up && next > 1) anime.animate(track, { scaleY: [1.7, 1], duration: 450, ease: 'out(3)' });
}
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)); }, 150 + Math.random() * 120]); })(i);
}
steps.push([function () {}, 1600]);
for (i = pw.length - 1; i >= 0; i--) {
(function (n) { steps.push([function () { set(pw.slice(0, n)); }, 45]); })(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-strength-meter]').forEach(function (el) {
if (el._psm) return;
el._psm = true;
setup(el);
});
}
window.animacious = window.animacious || {};
window.animacious.passwordStrength = strength;
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><div data-password-strength-meter>
<input type="password" aria-label="Password">
</div>