← Forms

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

  1. Paste the snippet just before </head>.
  2. Wrap a password input in an element with data-password-strength-meter.
  3. The meter is added under the field.

Options

Add any of these attributes to the same element.

data-labelsThe four words, comma-separated. Default Weak,Fair,Good,Strong.
data-colorsThe four colours, comma-separated. Default red, orange, amber, green.
data-autoplayIt 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-textWhat 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.

1 · Snippet
<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>
2 · Your HTML
<div data-password-strength-meter>
  <input type="password" aria-label="Password">
</div>