GODRICH
#css-animation#button#loader#transition#scroll#dashboard#ui#design-trends

Password Strength Meter UI: 9 Calculation Patterns

One strength value is set by how many character types mix in, one drops when the input matches a common value, and one finds where a confirm value first

Auto-plays · click a tile to jump to its section · all nine in one zip

None of the nine sections talks to a server — every strength, bit, or diff value below is computed in the browser from whatever the input currently holds. Nine sections wire ten real input listeners between them: eight add one listener each, and the confirm-match section (08) adds two, one per field, so a paste, a backspace, or a single keystroke recomputes the same value a typed character would. Every field stays type="text" and gets its dots from CSS -webkit-text-security: disc, not a native password field or a JavaScript mask. The looping preview each item shows on load steps through a fixed string array — there is no Math.random anywhere in these nine files — and a prefers-reduced-motion: reduce check turns that autoplay loop off without touching what a real keystroke does.

Anatomy of nine patterns — which value changes and when

Why two patterns switch stages at a set boundary value

01's four length thresholds and 06's six time-unit boundaries both work the same way: a continuous underlying number keeps changing on every keystroke, but the displayed stage or unit only updates at the instant that number crosses one of a short list of fixed points. Between two boundaries, typing a character does nothing to the label at all, even though the bar or bit value underneath is already moving.

Why four patterns compose the strength value from sub-values

02, 03, 07, and 09 all build one output value out of several smaller ones instead of reading a single measurement: a count of matched character-type patterns, a length-times-charset-size calculation, a true/false ratio across four criteria, or a half-length/half-class blend. None of the four ever looks at the raw input length alone.

Why three patterns subtract from or compare against a value instead

04 and 05 both start from the same length-based base score and subtract a penalty — a flat amount for 04's exact list match, a per-character amount for 05's repeat or sequence run — while 08 skips scoring altogether and instead compares two separate input values character by character to find where they stop agreeing.

Code per pattern

The interactive demo above each heading comes from the same file as the fenced snippet below it — a continuous slice of that item's own index.html, five to twenty-five lines, with every identifier and brace left exactly as written. Read together, these nine make a fairly complete set of password strength meter examples with the calculation exposed instead of hidden behind a library.

01Length threshold fill — the strength value steps up each time the input's character-count value crosses a set boundary value

Each time the input's character-count value climbs past one of four set boundary values — 4, 8, 12, or 16 — the strength value steps up to the next of five stages, and the fill bar snaps to match. A signup form's password field or an account setting that enforces a minimum length reads this way, since only the boundary crossing matters, not the exact count in between.

Length threshold fill — the strength value steps up each time the input's character-count value crosses a set boundary value
Open in a new tab

01 · Length threshold fill — the strength value steps up each time the input's character-count value crosses a set boundary value — Measured area 3.317% · moving frames 15/23Original by GODRICH · SCSS and React ship in the zip · See the password

THRESHOLDS = [4,8,12,16]stageOf(len)STAGE_CLASS

What the numbers say

Across 24 tracked frames, 01 moves on 15 of them, covering a 3.317% cumulative area at a 103.7 average intensity — motion is sparse because the fill only redraws at four threshold crossings instead of on every frame.

  // Computed layer: each time the length value crosses one of these boundary values, the strength value (0-4) steps up by one stage.
  var THRESHOLDS = [4, 8, 12, 16];
  var STAGE_CLASS = ['pw--s0', 'pw--s1', 'pw--s2', 'pw--s3', 'pw--s4'];

  function stageOf(len) {
    var s = 0;
    for (var i = 0; i < THRESHOLDS.length; i++) { if (len >= THRESHOLDS[i]) s = i + 1; }
    return s;
  }

  function paint(value) {
    var len = value.length;
    var stage = stageOf(len);
    var ratio = stage / THRESHOLDS.length;
    fill.style.transform = 'scaleX(' + ratio + ')';
    STAGE_CLASS.forEach(function (c) { root.classList.remove(c); });
    root.classList.add(STAGE_CLASS[stage]);
    label.textContent = 'Boundary ' + stage + '/4';
    lenEl.textContent = String(len);
  }

02Character-class count fill — the strength value is set by how many distinct character-type values are mixed into the input

The strength value here comes from how many of four character-type patterns — lowercase, uppercase, digit, symbol — test true against the current input, out of a possible four. An account setting that demands mixed case and symbols, or an internal system's password-change form, uses exactly this count to decide how many of its four badges light up.

Character-class count fill — the strength value is set by how many distinct character-type values are mixed into the input
Open in a new tab

02 · Character-class count fill — the strength value is set by how many distinct character-type values are mixed into the input — Measured area 9.395% · moving frames 22/23Original by GODRICH · SCSS and React ship in the zip · See the password

CLASSES 정규식 4종countClasses(value).cc__badge--on

What the numbers say

Across 24 tracked frames, 02 moves on 22 of them, covering a 9.395% cumulative area at a 119.4 average intensity — the highest cumulative area of the nine, since four badges and a fill bar all repaint together on nearly every step of the sequence.

  // Computed layer: the input value is tested against four character-type regular expressions, and the count of true results is tallied.
  var CLASSES = [/[a-z]/, /[A-Z]/, /[0-9]/, /[^A-Za-z0-9]/];

  function countClasses(value) {
    var n = 0;
    for (var i = 0; i < CLASSES.length; i++) {
      var hit = CLASSES[i].test(value);
      badges[i].classList.toggle('cc__badge--on', hit);
      if (hit) n++;
    }
    return n;
  }

  function paint(value) {
    var n = countClasses(value);
    fill.style.transform = 'scaleX(' + (n / CLASSES.length) + ')';
    label.textContent = 'Char types ' + n + '/4';
  }

03Entropy-bit value display — the strength value comes from a calculation multiplying the character-set size value by the length value

A bit value is computed by multiplying the input's length value by the size of whichever character sets it draws from, then taking a base-2 logarithm of that product. A developer-facing setting that reports a numeric security level, or a secret-value strength readout on an API-key screen, shows this bit value directly instead of a vague label.

Entropy-bit value display — the strength value comes from a calculation multiplying the character-set size value by the length value
Open in a new tab

03 · Entropy-bit value display — the strength value comes from a calculation multiplying the character-set size value by the length value — Measured area 4.297% · moving frames 22/23Original by GODRICH · SCSS and React ship in the zip · See the password

charsetSize(value)bitsOf(value)MAX_BITS = 100

What the numbers say

Across 24 tracked frames, 03 moves on 22 of them, covering a 4.297% cumulative area at a 183.8 average intensity — the fill keeps climbing on almost every frame because the bit value grows continuously with both length and character-set size, not in a few fixed jumps.

  // Computed layer: the mixed character types set the character-set size value, which is multiplied by the length value,
  // then a base-2 logarithm turns the product into a bit value (entropy = length × log2(character-set size)).
  function charsetSize(value) {
    var size = 0;
    if (/[a-z]/.test(value)) size += 26;
    if (/[A-Z]/.test(value)) size += 26;
    if (/[0-9]/.test(value)) size += 10;
    if (/[^A-Za-z0-9]/.test(value)) size += 32;
    return size;
  }

  function bitsOf(value) {
    var size = charsetSize(value);
    if (!value.length || size === 0) return { bits: 0, size: size };
    return { bits: value.length * (Math.log(size) / Math.log(2)), size: size };
  }

  function paint(value) {
    var r = bitsOf(value);
    var bits = Math.round(r.bits);
    bitsEl.textContent = String(bits);
    fill.style.transform = 'scaleX(' + Math.min(1, r.bits / MAX_BITS) + ')';
    setEl.textContent = String(r.size);
    lenEl.textContent = String(value.length);
  }

04Common-value penalty — the strength value is docked when the input value matches an entry in a preset common-value list

When the input value exactly matches one entry in a small preset list of common values, a fixed penalty is subtracted from an otherwise length-based score. A signup form that discourages reused, easy passwords, or an account setting that screens out commonly leaked values, uses this exact-match penalty as its whole mechanism.

Common-value penalty — the strength value is docked when the input value matches an entry in a preset common-value list
Open in a new tab

04 · Common-value penalty — the strength value is docked when the input value matches an entry in a preset common-value list — Measured area 3.392% · moving frames 21/23Original by GODRICH · SCSS and React ship in the zip · See the password

COMMON 배열 10종PENALTY = 40.cl--hit

What the numbers say

Across 24 tracked frames, 04 moves on 21 of them, covering a 3.392% cumulative area at a 105.5 average intensity — the fill jumps down sharply only on the two frames where the sequence's value exactly matches a common-list entry.

  // Computed layer: a small fictional common-value list (10 generic patterns, not a real leaked-value list) — zero real service names.
  var COMMON = ['password', '123456', 'qwerty', '111111', 'abc123',
                'letmein', 'iloveyou', 'admin', 'welcome', 'monkey'];
  var PENALTY = 40;

  function scoreOf(value) {
    var base = Math.min(100, value.length * 8);
    var hit = value.length > 0 && COMMON.indexOf(value.toLowerCase()) !== -1;
    var score = hit ? Math.max(0, base - PENALTY) : base;
    return { score: score, hit: hit };
  }

  function paint(value) {
    var r = scoreOf(value);
    fill.style.transform = 'scaleX(' + (r.score / 100) + ')';
    root.classList.toggle('cl--hit', r.hit);
    scoreEl.textContent = String(r.score);
    warn.style.opacity = r.hit ? '1' : '0';
    warn.style.transform = r.hit ? 'scale(1)' : 'scale(.7)';
  }

05Repeat/sequence penalty — the strength value drops by an amount tied to a run of the same character or a sequential digit value

The strength value drops by an amount tied to how many characters sit inside a run of three or more identical characters, or a run of three or more sequential character codes. An account security setting that filters out values like 1234 or aaaa, or a signup form that detects patterned input, relies on this run-length penalty.

Repeat/sequence penalty — the strength value drops by an amount tied to a run of the same character or a sequential digit value
Open in a new tab

05 · Repeat/sequence penalty — the strength value drops by an amount tied to a run of the same character or a sequential digit value — Measured area 3.086% · moving frames 22/23Original by GODRICH · SCSS and React ship in the zip · See the password

flagPositions(value)PER_CHAR_PENALTY = 8.rs__chip--on

What the numbers say

Across 24 tracked frames, 05 moves on 22 of them, covering a 3.086% cumulative area at a 135.5 average intensity — chips toggle on and off across most frames as the sequence alternates between a patterned value and a mixed one.

  function flagPositions(value) {
    var n = value.length;
    var flag = new Array(n).fill(false);
    var i = 0;
    while (i < n) {
      var j = i;
      while (j + 1 < n && value.charCodeAt(j + 1) === value.charCodeAt(j)) j++;
      if (j - i + 1 >= 3) for (var k = i; k <= j; k++) flag[k] = true;
      i = j + 1;
    }
    i = 0;
    while (i < n - 1) {
      var dir = value.charCodeAt(i + 1) - value.charCodeAt(i);
      if (dir === 1 || dir === -1) {
        var j2 = i + 1;
        while (j2 + 1 < n && value.charCodeAt(j2 + 1) - value.charCodeAt(j2) === dir) j2++;
        if (j2 - i + 1 >= 3) for (var k2 = i; k2 <= j2; k2++) flag[k2] = true;
        i = j2;
      } else {
        i++;
      }
    }
    return flag;
  }

06Estimated-time unit switch — the unit of a displayed time-estimate value switches each time a computed bit value passes a boundary value

A combination count comes from raising the input's character-set size to the power of its length, divided by an assumed guesses-per-second constant, then converted into a displayed time unit that switches as the underlying bit value crosses a boundary. A developer-facing secret-issuance tool or an account settings screen that builds security awareness uses this converted estimate as its headline value.

Estimated-time unit switch — the unit of a displayed time-estimate value switches each time a computed bit value passes a boundary value
Open in a new tab

06 · Estimated-time unit switch — the unit of a displayed time-estimate value switches each time a computed bit value passes a boundary value — Measured area 7.119% · moving frames 14/23Original by GODRICH · SCSS and React ship in the zip · See the password

GUESSES_PER_SEC = 1e6guessesOf(value)UNITS 단위 배열

What the numbers say

Across 24 tracked frames, 06 moves on 14 of them, covering a 7.119% cumulative area at a 179.7 average intensity — the unit label changes on only a few of the frames since it switches solely when the computed bit value crosses one of six set boundaries, a limit familiar to anyone who has tuned password strength css transitions before.

  // Computed layer: the character-set size raised to the length value gives the combination count, which is divided by an
  // assumed per-second guess rate to get a seconds value, then converted into unit steps (sec→min→hr→day→yr→century).
  function guessesOf(value) {
    var size = charsetSize(value);
    if (!value.length || size === 0) return 0;
    return Math.pow(size, value.length);
  }

  var UNITS = [
    { at: 1, name: 'sec' },
    { at: 60, name: 'min', div: 60 },
    { at: 3600, name: 'hr', div: 3600 },
    { at: 86400, name: 'day', div: 86400 },
    { at: 31536000, name: 'yr', div: 31536000 },
    { at: 31536000 * 100, name: 'century', div: 31536000 * 100 }
  ];

07Live criteria-ratio reflection — the strength value follows the ratio of condition values that turn true out of the total

Four independent criteria are each evaluated as true or false on every input, and the ratio of how many came back true against the total feeds the strength value as a fraction. A password strength meter ui that lists its requirements item by item, or an account setting that enumerates internal policy conditions, pairs this ratio with the four checkmarks it drives.

Live criteria-ratio reflection — the strength value follows the ratio of condition values that turn true out of the total
Open in a new tab

07 · Live criteria-ratio reflection — the strength value follows the ratio of condition values that turn true out of the total — Measured area 4.904% · moving frames 20/23Original by GODRICH · SCSS and React ship in the zip · See the password

CHECKS 기준 4종.cx__item--onSVG path 체크 아이콘

What the numbers say

Across 24 tracked frames, 07 moves on 20 of them, covering a 4.904% cumulative area at a 136.7 average intensity — the bar and the four checkmarks repaint together whenever the sequence adds or removes one qualifying character.

  // Computed layer: each of the four criteria is evaluated and the count of true results is tallied, then that ratio (count/4)
  // sets the bar fill — each badge is switched on by its own criterion's true/false, not by the count.
  var CHECKS = {
    len: function (v) { return v.length >= 8; },
    upper: function (v) { return /[A-Z]/.test(v); },
    digit: function (v) { return /[0-9]/.test(v); },
    special: function (v) { return /[^A-Za-z0-9]/.test(v); }
  };

  function paint(value) {
    var met = 0;
    items.forEach(function (li) {
      var key = li.getAttribute('data-key');
      var ok = CHECKS[key](value);
      li.classList.toggle('cx__item--on', ok);
      if (ok) met++;
    });
    fill.style.transform = 'scaleX(' + (met / items.length) + ')';
  }

08Live confirm-match diff — the position value where a confirmation input first diverges from the original value is found in real time

The confirmation input and the original are compared character by character from the start, and two separate listeners, one per field, recompute in real time the position value where they first diverge. A signup form's confirm-password field, or a reset screen's match check, depends on this diverging-position value rather than a simple equal-or-not comparison.

Live confirm-match diff — the position value where a confirmation input first diverges from the original value is found in real time
Open in a new tab

08 · Live confirm-match diff — the position value where a confirmation input first diverges from the original value is found in real time — Measured area 1.86% · moving frames 7/23Original by GODRICH · SCSS and React ship in the zip · See the password

matchLenOf(a, b).md__chip--bad.md--full

What the numbers say

Across 24 tracked frames, 08 moves on 7 of them, the fewest of the nine, covering a 1.86% cumulative area at a 125.0 average intensity — because the fixed password stays put while only the confirm field advances one character at a time through the sequence.

  function paint() {
    var a = pw.value, b = confirm.value;
    var matchLen = matchLenOf(a, b);
    var full = b.length > 0 && a.length === b.length && matchLen === a.length;

    chipsBox.innerHTML = '';
    for (var i = 0; i < b.length; i++) {
      var chip = document.createElement('span');
      chip.className = 'md__chip ' + (i < matchLen ? 'md__chip--ok' : 'md__chip--bad');
      chipsBox.appendChild(chip);
    }

    if (b.length === 0) statusEl.textContent = 'Waiting for input';
    else if (full) statusEl.textContent = b.length + ' chars fully match';
    else if (matchLen === b.length) statusEl.textContent = matchLen + ' chars match so far · keep typing';
    else statusEl.textContent = matchLen + ' chars match so far · ' + (matchLen + 1) + 'th position mismatch';

    root.classList.toggle('md--full', full);
  }

09Continuous color-value morph — the strength value changes continuously instead of jumping between stages, with its mapped color value shifting smoothly along

Instead of jumping between fixed stages, the strength value here changes continuously from a blended length-and-class ratio, and a mapped color value shifts smoothly along a three-stop gradient as that ratio moves. An account settings screen built for fine-grained visual feedback, or a password strength meter design that favors gradients over stepped bars, uses this continuous value in place of a stepped one.

Continuous color-value morph — the strength value changes continuously instead of jumping between stages, with its mapped color value shifting smoothly along
Open in a new tab

09 · Continuous color-value morph — the strength value changes continuously instead of jumping between stages, with its mapped color value shifting smoothly along — Measured area 3.354% · moving frames 22/23Original by GODRICH · SCSS and React ship in the zip · See the password

STOPS 색 정지점 3개colorAt(t)scoreT(value)

What the numbers say

Across 24 tracked frames, 09 moves on 22 of them, covering a 3.354% cumulative area at a 90.9 average intensity, the lowest of the nine — both the fill width and its interpolated color shift on nearly every frame, yet the bar itself stays a thin, low-contrast shape against the stage.

  // Computed layer: the length ratio and character-type ratio are mixed half and half into a continuous value (0-1), and that value
  // is linearly interpolated in RGB between three color stops (red→yellow→mint), recalculated on every input.
  var STOPS = [
    { t: 0,    rgb: [194, 54, 26] },
    { t: 0.5,  rgb: [255, 210, 63] },
    { t: 1,    rgb: [76, 212, 166] }
  ];

  function lerp(a, b, f) { return a + (b - a) * f; }

  function colorAt(t) {
    t = Math.max(0, Math.min(1, t));
    var lo = STOPS[0], hi = STOPS[STOPS.length - 1];
    for (var i = 0; i < STOPS.length - 1; i++) {
      if (t >= STOPS[i].t && t <= STOPS[i + 1].t) { lo = STOPS[i]; hi = STOPS[i + 1]; break; }
    }
    var span = hi.t - lo.t || 1;
    var f = (t - lo.t) / span;
    var r = Math.round(lerp(lo.rgb[0], hi.rgb[0], f));
    var g = Math.round(lerp(lo.rgb[1], hi.rgb[1], f));
    var b = Math.round(lerp(lo.rgb[2], hi.rgb[2], f));
    return 'rgb(' + r + ',' + g + ',' + b + ')';
  }

Where values reshape a pattern — boundary, penalty, and rate constants

Why 01's threshold positions decide which length earns which stage, not how many stages exist

Moving the four boundary values from 4/8/12/16 up to 6/10/14/18 does not add or remove a stage — there are still five — it only raises how many characters a given stage now requires, so an eight-character value that used to read as stage two now reads as stage one.

Why 04's penalty size decides how far a flagged value's score falls, not whether it gets flagged

Raising 04's penalty constant from 40 to 70 does not change which values match the common list — the exact-match check stays the same — it only changes how far the score falls once a match happens, from a value that still leaves some points on the board to one that drops most short common values to zero.

Why 06's guesses-per-second constant decides which unit a time reads in, not the underlying combination count

Raising 06's assumed guess rate from 1e6 to 1e9 leaves the combination count itself untouched — the same input still produces the same number of possible values — it only divides that count by a larger assumed rate, so the displayed estimate reads in a smaller unit, seconds or days instead of years.

These same three constants also mark where this post's nine patterns connect to others already covered here. Login/signup card UI patterns (206) computes a submit button's enabled value from whether every field is simply filled in, not from any graded score the way these nine strength values are. Form validation feedback (225) computes a single field's message-shown value from that one field's own pass-or-fail check, rather than blending several sub-values the way 02, 03, 07, and 09 do. Form validation UI (254) computes a page-wide summary value from every field's aggregate state at once, instead of the single live per-keystroke position value that 08 tracks. The zip behind this post packs all nine index.html and style.scss files together, opened with the password 84k56vcx, exactly as read above.

Where it breaks — a near-miss paste, a very long value, and non-Latin characters

What happens when a common value is pasted with one character changed

04's penalty only fires on an exact, case-insensitive match against its ten-entry list, so pasting Password1 instead of password clears the check entirely and the score reads as if no common value were involved at all, even though the two strings differ by one substitution and one added digit.

What happens when an extremely long value is typed

03's bit value and 06's combination count both keep growing without limit as length increases, but their bars do not: 03 caps its fill at a 100-bit MAX_BITS ceiling and 06 caps its fill at a LOG_MAX of 12 on a base-10 log scale, so a sixty-character value and a thirty-character value of the same character set can show an identical, fully filled bar even though their underlying numbers are nowhere near equal.

What happens when the value contains non-Latin characters

02, 03, 06, 07, and 09 all classify characters with the same /[^A-Za-z0-9]/ pattern, so a Korean, Japanese, or accented character falls into the single leftover symbol bucket alongside ! and # — 03's charset-size calculation adds a flat 32 for that whole bucket no matter how many distinct symbols actually appear, understating the real character-set size of a non-Latin value.

Three variants and accessibility

Item Constant Values What changes
01 length threshold fill THRESHOLDS [4,8,12,16] → [6,10,14,18] Same five stages, but each one now needs a longer value to reach
04 common-value penalty PENALTY 40 → 70 Same match check, but a matched value's score falls further
06 crack-time estimate GUESSES_PER_SEC 1e6 → 1e9 Same combination count, but the displayed time reads in a smaller unit

None of the nine sections adds an aria-live region or a role attribute anywhere — the only ARIA present is aria-hidden="true" on the four decorative checkmark icons in 07 and the one in 08, keeping them out of a screen reader's announcement entirely. Every item's prefers-reduced-motion: reduce block only disables the CSS transition that drives its own fill, chip, or badge — it does not touch what happens the moment a real keystroke fires, since paint() still runs and still updates every value instantly on input.

FAQ

Does a password strength meter ui need a server to compute a score?

No — every calculation above, from a length threshold to an entropy-bit value, runs entirely in the browser against whatever the input field currently holds, with nothing sent anywhere.

Can the entropy-bit pattern and the crack-time pattern share one calculation?

They already share one: 06's combination count is the same character-set-size-to-the-power-of-length value that 03 turns into a bit value, just divided by an assumed guess rate and converted into a time unit instead of a bit count.

Do any of these nine store or send the typed password anywhere?

No — every field stays in memory only for as long as the tab is open, nothing is written to storage or a network request, and the CSS masking that hides each character never touches what value is actually held.

Sources: MDN — input event · MDN — RegExp · MDN — transition · MDN — prefers-reduced-motion · MDN — aria-live · MDN — color-mix

Enter the archive password

The password is inside this article. You will find it as you read.