Open demo

CSS code

HTML
<div class="demo-grid">
<div><span class="lbl">Segmented meter and checklist</span>
<form class="pw" action="#" method="post" data-pw-form aria-labelledby="kh-title">
  <div class="pw-brand"><span class="pw-mark"><svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="8" cy="15" r="4"/><path d="M11 12l9-9M17 6l3 3M14 9l2 2"/></svg></span>Keyhaven</div>
  <h2 class="pw-title" id="kh-title">Create your family vault</h2>
  <p class="pw-sub">Your master password unlocks every other password. We can't reset it for you, so make it one you'll remember.</p>
  <div class="pw-field">
    <label class="pw-label" for="kh-email">Email (required)</label>
    <input class="pw-input" id="kh-email" name="email" type="email" autocomplete="email" required>
  </div>
  <div class="pw-field" data-strength="kh-pw">
    <label class="pw-label" for="kh-pw">Master password (required)</label>
    <div class="pw-box">
      <input class="pw-input" id="kh-pw" name="new-password" type="password" autocomplete="new-password" required minlength="12"
        pattern="(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^A-Za-z0-9]).{12,}" aria-describedby="kh-pw-rules">
      <button class="pw-eye" type="button" aria-controls="kh-pw" aria-pressed="false" aria-label="Show password" data-pw-toggle hidden>
        <svg class="pw-show" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2 12s3.6-7 10-7 10 7 10 7-3.6 7-10 7S2 12 2 12z"/><circle cx="12" cy="12" r="3"/></svg>
        <svg class="pw-hide" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 3l18 18M10.6 5.1A10.7 10.7 0 0 1 12 5c6.4 0 10 7 10 7a17.6 17.6 0 0 1-2.9 3.9M6.6 6.6C3.8 8.4 2 12 2 12s3.6 7 10 7a10 10 0 0 0 5.4-1.6M9.9 9.9a3 3 0 0 0 4.2 4.2"/></svg>
      </button>
    </div>
    <div class="pw-strength" data-show hidden>
      <div class="pw-meter" data-meter data-level="0"><i></i><i></i><i></i><i></i></div>
      <p class="pw-word" aria-hidden="true">Strength: <b data-word>none</b></p>
    </div>
    <ul class="pw-rules" id="kh-pw-rules">
      <li data-rule="len12"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3.5 8.5l3 3 6-7"/></svg>At least 12 characters<span class="pw-sr" data-sr></span></li>
      <li data-rule="case"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3.5 8.5l3 3 6-7"/></svg>Upper and lower case<span class="pw-sr" data-sr></span></li>
      <li data-rule="number"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3.5 8.5l3 3 6-7"/></svg>A number<span class="pw-sr" data-sr></span></li>
      <li data-rule="symbol"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3.5 8.5l3 3 6-7"/></svg>A symbol, like ! or #<span class="pw-sr" data-sr></span></li>
    </ul>
    <p class="pw-sr" aria-live="polite" data-live></p>
  </div>
  <button class="pw-btn" type="submit">Create vault</button>
  <p class="pw-status" role="status" data-status></p>
</form>
</div>
<div><span class="lbl">Strength bar and confirm field</span>
<form class="pw" action="#" method="post" data-pw-form aria-labelledby="hb-title">
  <div class="pw-invite"><span class="pw-avatar" aria-hidden="true">MO</span><p><b>Mira Okafor</b> invited you to join <b>Harbourline Studio</b> as an editor.</p></div>
  <h2 class="pw-title" id="hb-title">Set your password</h2>
  <p class="pw-sub">Signing in as <b>[email protected]</b></p>
  <input type="email" name="username" autocomplete="username" value="[email protected]" hidden>
  <div class="pw-field" data-strength="hb-pw">
    <label class="pw-label" for="hb-pw">New password (required)</label>
    <input class="pw-input" id="hb-pw" name="new-password" type="password" autocomplete="new-password" required minlength="10"
      pattern="(?=.*\d)(?=.*[^A-Za-z0-9]).{10,}" aria-describedby="hb-pw-rules">
    <div class="pw-strength" data-show hidden>
      <div class="pw-bar" data-meter data-level="0"><span></span></div>
      <p class="pw-word" aria-hidden="true">Strength: <b data-word>none</b></p>
    </div>
    <ul class="pw-chips" id="hb-pw-rules">
      <li data-rule="len10"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3.5 8.5l3 3 6-7"/></svg>10+ characters<span class="pw-sr" data-sr></span></li>
      <li data-rule="number"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3.5 8.5l3 3 6-7"/></svg>A number<span class="pw-sr" data-sr></span></li>
      <li data-rule="symbol"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3.5 8.5l3 3 6-7"/></svg>A symbol<span class="pw-sr" data-sr></span></li>
    </ul>
    <p class="pw-sr" aria-live="polite" data-live></p>
  </div>
  <div class="pw-field">
    <label class="pw-label" for="hb-pw2">Confirm password (required)</label>
    <input class="pw-input" id="hb-pw2" name="confirm-password" type="password" autocomplete="new-password" required data-match="hb-pw" aria-describedby="hb-pw2-msg">
    <p class="pw-match" id="hb-pw2-msg" aria-live="polite"></p>
  </div>
  <button class="pw-btn" type="submit">Join Harbourline Studio</button>
  <p class="pw-status" role="status" data-status></p>
</form>
</div>
</div>
CSS
/* Password strength and requirements. The requirement list is plain text that describes the field, so the
   form works without JavaScript (pattern and minlength enforce the rules); the script ticks the rules,
   fills the meter and announces a short summary through a polite live region once typing pauses. */
.pw{--pw-accent:#4338ca;--pw-accent-dark:#3730a3;--pw-ring:rgba(67,56,202,.22);--pw-ink:#111827;--pw-muted:#4b5563;--pw-line:#7c8291;--pw-track:#e3e5ec;
  --pw-weak:#b91c1c;--pw-fair:#b45309;--pw-good:#1d4ed8;--pw-strong:#15803d;
  background:#fff;color:var(--pw-ink);border-radius:18px;padding:32px 30px 28px;font-size:.92rem;line-height:1.45;
  box-shadow:0 1px 2px rgba(17,24,39,.06),0 14px 34px -14px rgba(30,27,75,.25)}
.pw-brand{display:flex;align-items:center;gap:10px;font-weight:700;margin-bottom:20px}
.pw-mark{width:32px;height:32px;border-radius:50%;background:var(--pw-accent);color:#fff;display:grid;place-items:center}
.pw-title{font-size:1.4rem;font-weight:750;letter-spacing:-.02em;line-height:1.2}
.pw-sub{color:var(--pw-muted);margin:6px 0 20px}
.pw-field{margin-bottom:16px}
.pw-label{display:block;font-size:.84rem;font-weight:600;margin-bottom:6px}
.pw-input{display:block;width:100%;font:inherit;color:inherit;background:#fff;border:1px solid var(--pw-line);border-radius:10px;padding:11px 13px;
  transition:border-color .15s,box-shadow .15s}
.pw-input:focus{outline:none;border-color:var(--pw-accent);box-shadow:0 0 0 3px var(--pw-ring)}
.pw-box{display:flex;background:#fff;border:1px solid var(--pw-line);border-radius:10px;transition:border-color .15s,box-shadow .15s}
.pw-box:focus-within{border-color:var(--pw-accent);box-shadow:0 0 0 3px var(--pw-ring)}
.pw-box .pw-input,.pw-box .pw-input:focus{flex:1;min-width:0;border:0;box-shadow:none;background:transparent}
.pw-eye{flex:none;width:46px;border:0;background:none;color:var(--pw-muted);cursor:pointer;border-radius:0 10px 10px 0;display:grid;place-items:center}
.pw-eye:hover{color:var(--pw-ink)}
.pw-eye:focus-visible{outline:2px solid var(--pw-accent);outline-offset:-5px;border-radius:8px}
.pw-eye[hidden],.pw-eye[aria-pressed="true"] .pw-show,.pw-eye[aria-pressed="false"] .pw-hide{display:none}
/* segmented meter: data-level 1–4 colours that many segments */
.pw-strength{display:flex;align-items:center;gap:12px;margin-top:10px}
.pw-strength[hidden]{display:none}
.pw-meter{flex:1;display:flex;gap:5px}
.pw-meter i{flex:1;height:6px;border-radius:3px;background:var(--pw-track);transition:background-color .2s}
.pw-meter[data-level="1"] i:nth-child(-n+1){background:var(--pw-weak)}
.pw-meter[data-level="2"] i:nth-child(-n+2){background:var(--pw-fair)}
.pw-meter[data-level="3"] i:nth-child(-n+3){background:var(--pw-good)}
.pw-meter[data-level="4"] i{background:var(--pw-strong)}
.pw-word{font-size:.8rem;color:var(--pw-muted);min-width:7.5em;text-align:right}
.pw-word b{color:var(--pw-ink)}
/* requirement checklist */
.pw-rules{list-style:none;display:grid;grid-template-columns:1fr 1fr;gap:7px 14px;margin-top:12px;font-size:.82rem;color:var(--pw-muted)}
.pw-rules li{display:flex;align-items:center;gap:7px}
.pw-rules svg{flex:none;width:16px;height:16px;border-radius:50%;border:1.5px solid var(--pw-line);color:transparent;padding:2px;transition:background-color .2s,border-color .2s}
.pw-rules li[data-met="true"]{color:var(--pw-strong)}
.pw-rules li[data-met="true"] svg{background:var(--pw-strong);border-color:var(--pw-strong);color:#fff}
/* continuous bar and rule chips */
.pw-bar{height:6px;border-radius:3px;background:var(--pw-track);overflow:hidden;flex:1}
.pw-bar span{display:block;height:100%;width:0;border-radius:3px;transition:width .25s,background-color .25s}
.pw-bar[data-level="1"] span{width:25%;background:var(--pw-weak)}
.pw-bar[data-level="2"] span{width:50%;background:var(--pw-fair)}
.pw-bar[data-level="3"] span{width:75%;background:var(--pw-good)}
.pw-bar[data-level="4"] span{width:100%;background:var(--pw-strong)}
.pw-chips{list-style:none;display:flex;flex-wrap:wrap;gap:6px;margin-top:12px;font-size:.78rem}
.pw-chips li{display:flex;align-items:center;gap:5px;padding:4px 10px 4px 8px;border:1px solid var(--pw-line);border-radius:999px;color:var(--pw-muted);
  transition:background-color .2s,border-color .2s,color .2s}
.pw-chips svg{width:12px;height:12px;opacity:.35}
.pw-chips li[data-met="true"]{background:#ecfdf3;border-color:var(--pw-strong);color:var(--pw-strong);font-weight:600}
.pw-chips li[data-met="true"] svg{opacity:1}
.pw-match{display:flex;align-items:center;gap:6px;margin-top:6px;font-size:.8rem;font-weight:600;min-height:1.2em}
.pw-match[data-state="ok"]{color:var(--pw-strong)}
.pw-match[data-state="no"]{color:var(--pw-weak)}
.pw-match[data-state="ok"]::before{content:"✓"}
.pw-match[data-state="no"]::before{content:"×"}
.pw-invite{display:flex;gap:12px;align-items:center;background:#f3f4fb;border-radius:12px;padding:12px 14px;margin-bottom:20px;font-size:.86rem}
.pw-avatar{flex:none;width:38px;height:38px;border-radius:50%;background:#c7d2fe;color:#312e81;font-weight:700;display:grid;place-items:center;font-size:.82rem}
.pw-btn{display:block;width:100%;font:inherit;font-weight:600;color:#fff;background:var(--pw-accent);border:0;border-radius:10px;padding:12px 16px;cursor:pointer;
  margin-top:6px;transition:background-color .15s}
.pw-btn:hover{background:var(--pw-accent-dark)}
.pw-btn:focus-visible{outline:2px solid var(--pw-accent);outline-offset:3px}
.pw-status:not(:empty){margin-top:14px;padding:10px 12px;border-radius:10px;background:#f0fdf4;color:#166534;font-weight:600;font-size:.84rem}
.pw-sr{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}
@media (max-width:420px){.pw{padding:26px 20px 22px}.pw-rules{grid-template-columns:1fr}}
@media (prefers-reduced-motion:reduce){.pw-input,.pw-box,.pw-meter i,.pw-rules svg,.pw-bar span,.pw-chips li,.pw-btn{transition:none}}
JavaScript
// Password rules, strength meter and confirm-match. No libraries. The level counts the rules met, so swap in a
// proper estimator if you need one. Live text waits until typing pauses so screen readers aren't flooded.
(function () {
  var RULES = {
    len12: function (v) { return v.length >= 12; },
    len10: function (v) { return v.length >= 10; },
    case: function (v) { return /[a-z]/.test(v) && /[A-Z]/.test(v); },
    number: function (v) { return /\d/.test(v); },
    symbol: function (v) { return /[^A-Za-z0-9]/.test(v); }
  };
  var WORDS = ['none', 'weak', 'fair', 'good', 'strong'];
  document.querySelectorAll('[data-strength]').forEach(function (box) {
    var input = document.getElementById(box.dataset.strength), rules = box.querySelectorAll('[data-rule]');
    var meter = box.querySelector('[data-meter]'), word = box.querySelector('[data-word]'), live = box.querySelector('[data-live]'), timer;
    box.querySelector('[data-show]').hidden = false;
    input.addEventListener('input', function () {
      var v = input.value, met = 0;
      rules.forEach(function (li) {
        var ok = RULES[li.dataset.rule](v);
        li.dataset.met = ok; li.querySelector('[data-sr]').textContent = ok ? ' (done)' : '';
        if (ok) met++;
      });
      var level = v ? Math.max(1, Math.round(met / rules.length * 4)) : 0;
      meter.dataset.level = level; word.textContent = WORDS[level];
      clearTimeout(timer);
      timer = setTimeout(function () {
        live.textContent = v ? 'Strength ' + WORDS[level] + ', ' + met + ' of ' + rules.length + ' requirements met.' : '';
      }, 900);
    });
  });
  document.querySelectorAll('[data-match]').forEach(function (confirm) {
    var first = document.getElementById(confirm.dataset.match), out = document.getElementById(confirm.id + '-msg');
    function update() {
      var same = confirm.value === first.value, msg = !confirm.value ? '' : same ? 'Passwords match.' : 'Passwords do not match yet.';
      confirm.setCustomValidity(confirm.value && !same ? 'The passwords do not match.' : '');
      if (out.textContent !== msg) { out.textContent = msg; out.dataset.state = !msg ? '' : same ? 'ok' : 'no'; }
    }
    confirm.addEventListener('input', update); first.addEventListener('input', update);
  });
  document.querySelectorAll('[data-pw-toggle]').forEach(function (btn) {
    var input = document.getElementById(btn.getAttribute('aria-controls'));
    btn.hidden = false;
    btn.addEventListener('click', function () {
      var show = input.type === 'password';
      input.type = show ? 'text' : 'password';
      btn.setAttribute('aria-pressed', show ? 'true' : 'false');
    });
  });
  // the browser validates first; submit only fires once every rule passes. Demo only: nothing is sent.
  document.querySelectorAll('[data-pw-form]').forEach(function (form) {
    form.addEventListener('submit', function (e) {
      e.preventDefault();
      form.querySelector('[data-status]').textContent = 'Password saved. You can sign in now.';
    });
  });
})();

Bootstrap 5 code

Requires: Bootstrap 5.3.8 CSS

HTML
<div class="demo-grid">
<div><span class="lbl">Segmented meter and checklist</span>
<form class="card pw" action="#" method="post" data-pw-form aria-labelledby="kh-title">
  <div class="pw-brand"><span class="pw-mark"><svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="8" cy="15" r="4"/><path d="M11 12l9-9M17 6l3 3M14 9l2 2"/></svg></span>Keyhaven</div>
  <h2 class="pw-title" id="kh-title">Create your family vault</h2>
  <p class="pw-sub">Your master password unlocks every other password. We can't reset it for you, so make it one you'll remember.</p>
  <div class="mb-3">
    <label class="form-label" for="kh-email">Email (required)</label>
    <input class="form-control" id="kh-email" name="email" type="email" autocomplete="email" required>
  </div>
  <div class="mb-3" data-strength="kh-pw">
    <label class="form-label" for="kh-pw">Master password (required)</label>
    <div class="input-group">
      <input class="form-control" id="kh-pw" name="new-password" type="password" autocomplete="new-password" required minlength="12"
        pattern="(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^A-Za-z0-9]).{12,}" aria-describedby="kh-pw-rules">
      <button class="btn pw-eye" type="button" aria-controls="kh-pw" aria-pressed="false" aria-label="Show password" data-pw-toggle hidden>
        <svg class="pw-show" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2 12s3.6-7 10-7 10 7 10 7-3.6 7-10 7S2 12 2 12z"/><circle cx="12" cy="12" r="3"/></svg>
        <svg class="pw-hide" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 3l18 18M10.6 5.1A10.7 10.7 0 0 1 12 5c6.4 0 10 7 10 7a17.6 17.6 0 0 1-2.9 3.9M6.6 6.6C3.8 8.4 2 12 2 12s3.6 7 10 7a10 10 0 0 0 5.4-1.6M9.9 9.9a3 3 0 0 0 4.2 4.2"/></svg>
      </button>
    </div>
    <div class="pw-strength" data-show hidden>
      <div class="pw-meter" data-meter data-level="0"><i></i><i></i><i></i><i></i></div>
      <p class="pw-word" aria-hidden="true">Strength: <b data-word>none</b></p>
    </div>
    <ul class="list-unstyled pw-rules" id="kh-pw-rules">
      <li data-rule="len12"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3.5 8.5l3 3 6-7"/></svg>At least 12 characters<span class="visually-hidden" data-sr></span></li>
      <li data-rule="case"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3.5 8.5l3 3 6-7"/></svg>Upper and lower case<span class="visually-hidden" data-sr></span></li>
      <li data-rule="number"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3.5 8.5l3 3 6-7"/></svg>A number<span class="visually-hidden" data-sr></span></li>
      <li data-rule="symbol"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3.5 8.5l3 3 6-7"/></svg>A symbol, like ! or #<span class="visually-hidden" data-sr></span></li>
    </ul>
    <p class="visually-hidden" aria-live="polite" data-live></p>
  </div>
  <button class="btn btn-primary w-100" type="submit">Create vault</button>
  <p class="pw-status" role="status" data-status></p>
</form>
</div>
<div><span class="lbl">Strength bar and confirm field</span>
<form class="card pw" action="#" method="post" data-pw-form aria-labelledby="hb-title">
  <div class="pw-invite"><span class="pw-avatar" aria-hidden="true">MO</span><p><b>Mira Okafor</b> invited you to join <b>Harbourline Studio</b> as an editor.</p></div>
  <h2 class="pw-title" id="hb-title">Set your password</h2>
  <p class="pw-sub">Signing in as <b>[email protected]</b></p>
  <input type="email" name="username" autocomplete="username" value="[email protected]" hidden>
  <div class="mb-3" data-strength="hb-pw">
    <label class="form-label" for="hb-pw">New password (required)</label>
    <input class="form-control" id="hb-pw" name="new-password" type="password" autocomplete="new-password" required minlength="10"
      pattern="(?=.*\d)(?=.*[^A-Za-z0-9]).{10,}" aria-describedby="hb-pw-rules">
    <div class="pw-strength" data-show hidden>
      <div class="progress pw-bar" data-meter data-level="0" aria-hidden="true"><div class="progress-bar"></div></div>
      <p class="pw-word" aria-hidden="true">Strength: <b data-word>none</b></p>
    </div>
    <ul class="list-unstyled pw-chips" id="hb-pw-rules">
      <li class="badge rounded-pill" data-rule="len10"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3.5 8.5l3 3 6-7"/></svg>10+ characters<span class="visually-hidden" data-sr></span></li>
      <li class="badge rounded-pill" data-rule="number"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3.5 8.5l3 3 6-7"/></svg>A number<span class="visually-hidden" data-sr></span></li>
      <li class="badge rounded-pill" data-rule="symbol"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3.5 8.5l3 3 6-7"/></svg>A symbol<span class="visually-hidden" data-sr></span></li>
    </ul>
    <p class="visually-hidden" aria-live="polite" data-live></p>
  </div>
  <div class="mb-3">
    <label class="form-label" for="hb-pw2">Confirm password (required)</label>
    <input class="form-control" id="hb-pw2" name="confirm-password" type="password" autocomplete="new-password" required data-match="hb-pw" aria-describedby="hb-pw2-msg">
    <p class="form-text pw-match" id="hb-pw2-msg" aria-live="polite"></p>
  </div>
  <button class="btn btn-primary w-100" type="submit">Join Harbourline Studio</button>
  <p class="pw-status" role="status" data-status></p>
</form>
</div>
</div>
CSS
/* Bootstrap .card, .form-control, .input-group, .progress, .badge and .btn-primary, recoloured through
   Bootstrap's CSS variables. The requirement list describes the field, so the form works without JavaScript
   (pattern and minlength enforce the rules); the script ticks rules, fills the meter and announces a summary. */
.pw{--pw-accent:#4338ca;--pw-accent-dark:#3730a3;--pw-ring:rgba(67,56,202,.22);--pw-ink:#111827;--pw-muted:#4b5563;--pw-line:#7c8291;--pw-track:#e3e5ec;
  --pw-weak:#b91c1c;--pw-fair:#b45309;--pw-good:#1d4ed8;--pw-strong:#15803d;
  --bs-card-border-width:0;--bs-card-border-radius:18px;--bs-border-color:var(--pw-line);--bs-border-radius:10px;--bs-body-color:var(--pw-ink);
  padding:32px 30px 28px;color:var(--pw-ink);font-size:.92rem;line-height:1.45;box-shadow:0 1px 2px rgba(17,24,39,.06),0 14px 34px -14px rgba(30,27,75,.25)}
.pw p,.pw h2{margin:0}
.pw .pw-brand{display:flex;align-items:center;gap:10px;font-weight:700;margin-bottom:20px}
.pw .pw-mark{width:32px;height:32px;border-radius:50%;background:var(--pw-accent);color:#fff;display:grid;place-items:center}
.pw .pw-title{font-size:1.4rem;font-weight:750;letter-spacing:-.02em;line-height:1.2}
.pw .pw-sub{color:var(--pw-muted);margin:6px 0 20px}
.pw .form-label{display:block;font-size:.84rem;font-weight:600;margin-bottom:6px}
.pw .form-control{font-size:inherit;line-height:inherit;color:var(--pw-ink);padding:11px 13px;transition:border-color .15s,box-shadow .15s}
.pw .form-control:focus{border-color:var(--pw-accent);box-shadow:0 0 0 3px var(--pw-ring)}
.pw .input-group{background:#fff;border:1px solid var(--pw-line);border-radius:10px;transition:border-color .15s,box-shadow .15s}
.pw .input-group:focus-within{border-color:var(--pw-accent);box-shadow:0 0 0 3px var(--pw-ring)}
.pw .input-group .form-control,.pw .input-group .form-control:focus{border:0;box-shadow:none;background:transparent}
.pw .pw-eye{width:46px;padding:0;border:0;color:var(--pw-muted);border-radius:0 10px 10px 0!important;display:grid;place-items:center}
.pw .pw-eye:hover{color:var(--pw-ink)}
.pw .pw-eye:focus-visible{box-shadow:none;outline:2px solid var(--pw-accent);outline-offset:-5px;border-radius:8px!important}
.pw .pw-eye[hidden],.pw .pw-eye[aria-pressed="true"] .pw-show,.pw .pw-eye[aria-pressed="false"] .pw-hide{display:none}
/* segmented meter: data-level 1–4 colours that many segments */
.pw .pw-strength{display:flex;align-items:center;gap:12px;margin-top:10px}
.pw .pw-strength[hidden]{display:none}
.pw .pw-meter{flex:1;display:flex;gap:5px}
.pw .pw-meter i{flex:1;height:6px;border-radius:3px;background:var(--pw-track);transition:background-color .2s}
.pw .pw-meter[data-level="1"] i:nth-child(-n+1){background:var(--pw-weak)}
.pw .pw-meter[data-level="2"] i:nth-child(-n+2){background:var(--pw-fair)}
.pw .pw-meter[data-level="3"] i:nth-child(-n+3){background:var(--pw-good)}
.pw .pw-meter[data-level="4"] i{background:var(--pw-strong)}
.pw .pw-word{font-size:.8rem;color:var(--pw-muted);min-width:7.5em;text-align:right}
.pw .pw-word b{color:var(--pw-ink)}
/* requirement checklist */
.pw .pw-rules{display:grid;grid-template-columns:1fr 1fr;gap:7px 14px;margin:12px 0 0;font-size:.82rem;color:var(--pw-muted)}
.pw .pw-rules li{display:flex;align-items:center;gap:7px}
.pw .pw-rules svg{flex:none;width:16px;height:16px;border-radius:50%;border:1.5px solid var(--pw-line);color:transparent;padding:2px;transition:background-color .2s,border-color .2s}
.pw .pw-rules li[data-met="true"]{color:var(--pw-strong)}
.pw .pw-rules li[data-met="true"] svg{background:var(--pw-strong);border-color:var(--pw-strong);color:#fff}
/* .progress as the continuous bar, .badge as rule chips */
.pw .pw-bar{--bs-progress-height:6px;--bs-progress-bg:var(--pw-track);--bs-progress-border-radius:3px;flex:1}
.pw .pw-bar .progress-bar{width:0;border-radius:3px;transition:width .25s,background-color .25s}
.pw .pw-bar[data-level="1"] .progress-bar{width:25%;background:var(--pw-weak)}
.pw .pw-bar[data-level="2"] .progress-bar{width:50%;background:var(--pw-fair)}
.pw .pw-bar[data-level="3"] .progress-bar{width:75%;background:var(--pw-good)}
.pw .pw-bar[data-level="4"] .progress-bar{width:100%;background:var(--pw-strong)}
.pw .pw-chips{display:flex;flex-wrap:wrap;gap:6px;margin:12px 0 0}
.pw .pw-chips .badge{--bs-badge-padding-x:10px;--bs-badge-padding-y:4px;--bs-badge-font-size:.78rem;--bs-badge-font-weight:400;--bs-badge-color:var(--pw-muted);
  display:flex;align-items:center;gap:5px;padding-left:8px;line-height:1.45;border:1px solid var(--pw-line);transition:background-color .2s,border-color .2s,color .2s}
.pw .pw-chips svg{width:12px;height:12px;opacity:.35}
.pw .pw-chips .badge[data-met="true"]{background:#ecfdf3;border-color:var(--pw-strong);color:var(--pw-strong);font-weight:600}
.pw .pw-chips .badge[data-met="true"] svg{opacity:1}
.pw .pw-match{display:flex;align-items:center;gap:6px;margin-top:6px;font-size:.8rem;font-weight:600;min-height:1.2em}
.pw .pw-match[data-state="ok"]{color:var(--pw-strong)}
.pw .pw-match[data-state="no"]{color:var(--pw-weak)}
.pw .pw-match[data-state="ok"]::before{content:"✓"}
.pw .pw-match[data-state="no"]::before{content:"×"}
.pw .pw-invite{display:flex;gap:12px;align-items:center;background:#f3f4fb;border-radius:12px;padding:12px 14px;margin-bottom:20px;font-size:.86rem}
.pw .pw-avatar{flex:none;width:38px;height:38px;border-radius:50%;background:#c7d2fe;color:#312e81;font-weight:700;display:grid;place-items:center;font-size:.82rem}
.pw .btn-primary{--bs-btn-bg:var(--pw-accent);--bs-btn-border-color:var(--pw-accent);--bs-btn-hover-bg:var(--pw-accent-dark);--bs-btn-hover-border-color:var(--pw-accent-dark);
  --bs-btn-active-bg:var(--pw-accent-dark);--bs-btn-active-border-color:var(--pw-accent-dark);--bs-btn-focus-box-shadow:none;
  --bs-btn-font-weight:600;--bs-btn-padding-y:11px;--bs-btn-font-size:.92rem;--bs-btn-border-radius:10px;line-height:1.45;margin-top:6px;display:block}
.pw .btn-primary:focus-visible{outline:2px solid var(--pw-accent);outline-offset:3px}
.pw .pw-status:not(:empty){margin-top:14px;padding:10px 12px;border-radius:10px;background:#f0fdf4;color:#166534;font-weight:600;font-size:.84rem}
.card.pw{display:block} /* plain block flow, so vertical margins match the CSS version */
@media (max-width:420px){.pw{padding:26px 20px 22px}.pw .pw-rules{grid-template-columns:1fr}}
@media (prefers-reduced-motion:reduce){.pw .form-control,.pw .input-group,.pw .pw-meter i,.pw .pw-rules svg,.pw .progress-bar,.pw .badge,.pw .btn{transition:none}}
JavaScript
// Password rules, strength meter and confirm-match. No libraries. The level counts the rules met, so swap in a
// proper estimator if you need one. Live text waits until typing pauses so screen readers aren't flooded.
(function () {
  var RULES = {
    len12: function (v) { return v.length >= 12; },
    len10: function (v) { return v.length >= 10; },
    case: function (v) { return /[a-z]/.test(v) && /[A-Z]/.test(v); },
    number: function (v) { return /\d/.test(v); },
    symbol: function (v) { return /[^A-Za-z0-9]/.test(v); }
  };
  var WORDS = ['none', 'weak', 'fair', 'good', 'strong'];
  document.querySelectorAll('[data-strength]').forEach(function (box) {
    var input = document.getElementById(box.dataset.strength), rules = box.querySelectorAll('[data-rule]');
    var meter = box.querySelector('[data-meter]'), word = box.querySelector('[data-word]'), live = box.querySelector('[data-live]'), timer;
    box.querySelector('[data-show]').hidden = false;
    input.addEventListener('input', function () {
      var v = input.value, met = 0;
      rules.forEach(function (li) {
        var ok = RULES[li.dataset.rule](v);
        li.dataset.met = ok; li.querySelector('[data-sr]').textContent = ok ? ' (done)' : '';
        if (ok) met++;
      });
      var level = v ? Math.max(1, Math.round(met / rules.length * 4)) : 0;
      meter.dataset.level = level; word.textContent = WORDS[level];
      clearTimeout(timer);
      timer = setTimeout(function () {
        live.textContent = v ? 'Strength ' + WORDS[level] + ', ' + met + ' of ' + rules.length + ' requirements met.' : '';
      }, 900);
    });
  });
  document.querySelectorAll('[data-match]').forEach(function (confirm) {
    var first = document.getElementById(confirm.dataset.match), out = document.getElementById(confirm.id + '-msg');
    function update() {
      var same = confirm.value === first.value, msg = !confirm.value ? '' : same ? 'Passwords match.' : 'Passwords do not match yet.';
      confirm.setCustomValidity(confirm.value && !same ? 'The passwords do not match.' : '');
      if (out.textContent !== msg) { out.textContent = msg; out.dataset.state = !msg ? '' : same ? 'ok' : 'no'; }
    }
    confirm.addEventListener('input', update); first.addEventListener('input', update);
  });
  document.querySelectorAll('[data-pw-toggle]').forEach(function (btn) {
    var input = document.getElementById(btn.getAttribute('aria-controls'));
    btn.hidden = false;
    btn.addEventListener('click', function () {
      var show = input.type === 'password';
      input.type = show ? 'text' : 'password';
      btn.setAttribute('aria-pressed', show ? 'true' : 'false');
    });
  });
  // the browser validates first; submit only fires once every rule passes. Demo only: nothing is sent.
  document.querySelectorAll('[data-pw-form]').forEach(function (form) {
    form.addEventListener('submit', function (e) {
      e.preventDefault();
      form.querySelector('[data-status]').textContent = 'Password saved. You can sign in now.';
    });
  });
})();

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<div class="grid grid-cols-[repeat(auto-fit,minmax(300px,1fr))] items-start gap-[34px]">
<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-[#565d6d] uppercase">Segmented meter and checklist</span>
<form class="rounded-[18px] bg-white px-[30px] pt-8 pb-7 text-[.92rem] leading-[1.45] text-pw-ink shadow-[0_1px_2px_rgba(17,24,39,.06),0_14px_34px_-14px_rgba(30,27,75,.25)] max-[420px]:px-5 max-[420px]:pt-[26px] max-[420px]:pb-[22px]" action="#" method="post" data-pw-form aria-labelledby="kh-title">
  <div class="mb-5 flex items-center gap-2.5 font-bold"><span class="grid size-8 place-items-center rounded-full bg-pw-accent text-white"><svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="8" cy="15" r="4"/><path d="M11 12l9-9M17 6l3 3M14 9l2 2"/></svg></span>Keyhaven</div>
  <h2 class="text-[1.4rem] leading-[1.2] font-[750] tracking-[-.02em]" id="kh-title">Create your family vault</h2>
  <p class="mt-1.5 mb-5 text-pw-muted">Your master password unlocks every other password. We can't reset it for you, so make it one you'll remember.</p>
  <div class="mb-4">
    <label class="mb-1.5 block text-[.84rem] font-semibold" for="kh-email">Email (required)</label>
    <input class="block w-full rounded-[10px] border border-pw-line bg-white px-[13px] py-[11px] transition-[border-color,box-shadow] duration-150 focus:border-pw-accent focus:shadow-[0_0_0_3px_rgba(67,56,202,.22)] focus:outline-none motion-reduce:transition-none" id="kh-email" name="email" type="email" autocomplete="email" required>
  </div>
  <div class="mb-4" data-strength="kh-pw">
    <label class="mb-1.5 block text-[.84rem] font-semibold" for="kh-pw">Master password (required)</label>
    <div class="flex rounded-[10px] border border-pw-line bg-white transition-[border-color,box-shadow] duration-150 focus-within:border-pw-accent focus-within:shadow-[0_0_0_3px_rgba(67,56,202,.22)] motion-reduce:transition-none">
      <input class="min-w-0 flex-1 rounded-l-[10px] bg-transparent px-[13px] py-[11px] focus:outline-none" id="kh-pw" name="new-password" type="password" autocomplete="new-password" required minlength="12"
        pattern="(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^A-Za-z0-9]).{12,}" aria-describedby="kh-pw-rules">
      <button class="group grid w-[46px] flex-none cursor-pointer place-items-center rounded-r-[10px] text-pw-muted hover:text-pw-ink focus-visible:rounded-lg focus-visible:outline-2 focus-visible:-outline-offset-5 focus-visible:outline-pw-accent" type="button" aria-controls="kh-pw" aria-pressed="false" aria-label="Show password" data-pw-toggle hidden>
        <svg class="group-aria-pressed:hidden" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2 12s3.6-7 10-7 10 7 10 7-3.6 7-10 7S2 12 2 12z"/><circle cx="12" cy="12" r="3"/></svg>
        <svg class="hidden group-aria-pressed:block" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 3l18 18M10.6 5.1A10.7 10.7 0 0 1 12 5c6.4 0 10 7 10 7a17.6 17.6 0 0 1-2.9 3.9M6.6 6.6C3.8 8.4 2 12 2 12s3.6 7 10 7a10 10 0 0 0 5.4-1.6M9.9 9.9a3 3 0 0 0 4.2 4.2"/></svg>
      </button>
    </div>
    <div class="mt-2.5 flex items-center gap-3" data-show hidden>
      <div class="group flex flex-1 gap-[5px]" data-meter data-level="0"><i class="h-1.5 flex-1 rounded-[3px] bg-pw-track transition-[background-color,border-color,color] duration-200 motion-reduce:transition-none group-data-[level=1]:bg-pw-weak group-data-[level=2]:bg-pw-fair group-data-[level=3]:bg-pw-good group-data-[level=4]:bg-pw-strong"></i><i class="h-1.5 flex-1 rounded-[3px] bg-pw-track transition-[background-color,border-color,color] duration-200 motion-reduce:transition-none group-data-[level=2]:bg-pw-fair group-data-[level=3]:bg-pw-good group-data-[level=4]:bg-pw-strong"></i><i class="h-1.5 flex-1 rounded-[3px] bg-pw-track transition-[background-color,border-color,color] duration-200 motion-reduce:transition-none group-data-[level=3]:bg-pw-good group-data-[level=4]:bg-pw-strong"></i><i class="h-1.5 flex-1 rounded-[3px] bg-pw-track transition-[background-color,border-color,color] duration-200 motion-reduce:transition-none group-data-[level=4]:bg-pw-strong"></i></div>
      <p class="min-w-[7.5em] text-right text-[.8rem] text-pw-muted" aria-hidden="true">Strength: <b class="text-pw-ink" data-word>none</b></p>
    </div>
    <ul class="mt-3 grid grid-cols-2 gap-x-3.5 gap-y-[7px] text-[.82rem] text-pw-muted max-[420px]:grid-cols-1" id="kh-pw-rules">
      <li class="group flex items-center gap-[7px] data-[met=true]:text-pw-strong" data-rule="len12"><svg class="size-4 flex-none rounded-full border-[1.5px] border-pw-line p-0.5 text-transparent transition-[background-color,border-color,color] duration-200 group-data-[met=true]:border-pw-strong group-data-[met=true]:bg-pw-strong group-data-[met=true]:text-white motion-reduce:transition-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3.5 8.5l3 3 6-7"/></svg>At least 12 characters<span class="sr-only" data-sr></span></li>
      <li class="group flex items-center gap-[7px] data-[met=true]:text-pw-strong" data-rule="case"><svg class="size-4 flex-none rounded-full border-[1.5px] border-pw-line p-0.5 text-transparent transition-[background-color,border-color,color] duration-200 group-data-[met=true]:border-pw-strong group-data-[met=true]:bg-pw-strong group-data-[met=true]:text-white motion-reduce:transition-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3.5 8.5l3 3 6-7"/></svg>Upper and lower case<span class="sr-only" data-sr></span></li>
      <li class="group flex items-center gap-[7px] data-[met=true]:text-pw-strong" data-rule="number"><svg class="size-4 flex-none rounded-full border-[1.5px] border-pw-line p-0.5 text-transparent transition-[background-color,border-color,color] duration-200 group-data-[met=true]:border-pw-strong group-data-[met=true]:bg-pw-strong group-data-[met=true]:text-white motion-reduce:transition-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3.5 8.5l3 3 6-7"/></svg>A number<span class="sr-only" data-sr></span></li>
      <li class="group flex items-center gap-[7px] data-[met=true]:text-pw-strong" data-rule="symbol"><svg class="size-4 flex-none rounded-full border-[1.5px] border-pw-line p-0.5 text-transparent transition-[background-color,border-color,color] duration-200 group-data-[met=true]:border-pw-strong group-data-[met=true]:bg-pw-strong group-data-[met=true]:text-white motion-reduce:transition-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3.5 8.5l3 3 6-7"/></svg>A symbol, like ! or #<span class="sr-only" data-sr></span></li>
    </ul>
    <p class="sr-only" aria-live="polite" data-live></p>
  </div>
  <button class="mt-1.5 block w-full cursor-pointer rounded-[10px] bg-pw-accent px-4 py-3 font-semibold text-white transition-[background-color,border-color,color] duration-150 hover:bg-pw-accent-dark focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-pw-accent motion-reduce:transition-none" type="submit">Create vault</button>
  <p class="text-[.84rem] font-semibold text-[#166534] not-empty:mt-3.5 not-empty:rounded-[10px] not-empty:bg-[#f0fdf4] not-empty:px-3 not-empty:py-2.5" role="status" data-status></p>
</form>
</div>
<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-[#565d6d] uppercase">Strength bar and confirm field</span>
<form class="rounded-[18px] bg-white px-[30px] pt-8 pb-7 text-[.92rem] leading-[1.45] text-pw-ink shadow-[0_1px_2px_rgba(17,24,39,.06),0_14px_34px_-14px_rgba(30,27,75,.25)] max-[420px]:px-5 max-[420px]:pt-[26px] max-[420px]:pb-[22px]" action="#" method="post" data-pw-form aria-labelledby="hb-title">
  <div class="mb-5 flex items-center gap-3 rounded-xl bg-[#f3f4fb] px-3.5 py-3 text-[.86rem]"><span class="grid size-[38px] flex-none place-items-center rounded-full bg-[#c7d2fe] text-[.82rem] font-bold text-[#312e81]" aria-hidden="true">MO</span><p><b>Mira Okafor</b> invited you to join <b>Harbourline Studio</b> as an editor.</p></div>
  <h2 class="text-[1.4rem] leading-[1.2] font-[750] tracking-[-.02em]" id="hb-title">Set your password</h2>
  <p class="mt-1.5 mb-5 text-pw-muted">Signing in as <b>[email protected]</b></p>
  <input type="email" name="username" autocomplete="username" value="[email protected]" hidden>
  <div class="mb-4" data-strength="hb-pw">
    <label class="mb-1.5 block text-[.84rem] font-semibold" for="hb-pw">New password (required)</label>
    <input class="block w-full rounded-[10px] border border-pw-line bg-white px-[13px] py-[11px] transition-[border-color,box-shadow] duration-150 focus:border-pw-accent focus:shadow-[0_0_0_3px_rgba(67,56,202,.22)] focus:outline-none motion-reduce:transition-none" id="hb-pw" name="new-password" type="password" autocomplete="new-password" required minlength="10"
      pattern="(?=.*\d)(?=.*[^A-Za-z0-9]).{10,}" aria-describedby="hb-pw-rules">
    <div class="mt-2.5 flex items-center gap-3" data-show hidden>
      <div class="group h-1.5 flex-1 overflow-hidden rounded-[3px] bg-pw-track" data-meter data-level="0"><span class="block h-full w-0 rounded-[3px] transition-[width,background-color] duration-250 group-data-[level=1]:w-1/4 group-data-[level=1]:bg-pw-weak group-data-[level=2]:w-1/2 group-data-[level=2]:bg-pw-fair group-data-[level=3]:w-3/4 group-data-[level=3]:bg-pw-good group-data-[level=4]:w-full group-data-[level=4]:bg-pw-strong motion-reduce:transition-none"></span></div>
      <p class="min-w-[7.5em] text-right text-[.8rem] text-pw-muted" aria-hidden="true">Strength: <b class="text-pw-ink" data-word>none</b></p>
    </div>
    <ul class="mt-3 flex flex-wrap gap-1.5 text-[.78rem]" id="hb-pw-rules">
      <li class="group flex items-center gap-[5px] rounded-full border border-pw-line py-1 pr-2.5 pl-2 text-pw-muted transition-[background-color,border-color,color] duration-200 data-[met=true]:border-pw-strong data-[met=true]:bg-[#ecfdf3] data-[met=true]:font-semibold data-[met=true]:text-pw-strong motion-reduce:transition-none" data-rule="len10"><svg class="size-3 opacity-35 group-data-[met=true]:opacity-100" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3.5 8.5l3 3 6-7"/></svg>10+ characters<span class="sr-only" data-sr></span></li>
      <li class="group flex items-center gap-[5px] rounded-full border border-pw-line py-1 pr-2.5 pl-2 text-pw-muted transition-[background-color,border-color,color] duration-200 data-[met=true]:border-pw-strong data-[met=true]:bg-[#ecfdf3] data-[met=true]:font-semibold data-[met=true]:text-pw-strong motion-reduce:transition-none" data-rule="number"><svg class="size-3 opacity-35 group-data-[met=true]:opacity-100" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3.5 8.5l3 3 6-7"/></svg>A number<span class="sr-only" data-sr></span></li>
      <li class="group flex items-center gap-[5px] rounded-full border border-pw-line py-1 pr-2.5 pl-2 text-pw-muted transition-[background-color,border-color,color] duration-200 data-[met=true]:border-pw-strong data-[met=true]:bg-[#ecfdf3] data-[met=true]:font-semibold data-[met=true]:text-pw-strong motion-reduce:transition-none" data-rule="symbol"><svg class="size-3 opacity-35 group-data-[met=true]:opacity-100" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3.5 8.5l3 3 6-7"/></svg>A symbol<span class="sr-only" data-sr></span></li>
    </ul>
    <p class="sr-only" aria-live="polite" data-live></p>
  </div>
  <div class="mb-4">
    <label class="mb-1.5 block text-[.84rem] font-semibold" for="hb-pw2">Confirm password (required)</label>
    <input class="block w-full rounded-[10px] border border-pw-line bg-white px-[13px] py-[11px] transition-[border-color,box-shadow] duration-150 focus:border-pw-accent focus:shadow-[0_0_0_3px_rgba(67,56,202,.22)] focus:outline-none motion-reduce:transition-none" id="hb-pw2" name="confirm-password" type="password" autocomplete="new-password" required data-match="hb-pw" aria-describedby="hb-pw2-msg">
    <p class="mt-1.5 flex min-h-[1.2em] items-center gap-1.5 text-[.8rem] font-semibold data-[state=no]:text-pw-weak data-[state=no]:before:content-['×'] data-[state=ok]:text-pw-strong data-[state=ok]:before:content-['✓']" id="hb-pw2-msg" aria-live="polite"></p>
  </div>
  <button class="mt-1.5 block w-full cursor-pointer rounded-[10px] bg-pw-accent px-4 py-3 font-semibold text-white transition-[background-color,border-color,color] duration-150 hover:bg-pw-accent-dark focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-pw-accent motion-reduce:transition-none" type="submit">Join Harbourline Studio</button>
  <p class="text-[.84rem] font-semibold text-[#166534] not-empty:mt-3.5 not-empty:rounded-[10px] not-empty:bg-[#f0fdf4] not-empty:px-3 not-empty:py-2.5" role="status" data-status></p>
</form>
</div>
</div>
CSS
@theme {
  --color-pw-accent: #4338ca;
  --color-pw-accent-dark: #3730a3;
  --color-pw-ink: #111827;
  --color-pw-muted: #4b5563;
  --color-pw-line: #7c8291;
  --color-pw-track: #e3e5ec;
  --color-pw-weak: #b91c1c;
  --color-pw-fair: #b45309;
  --color-pw-good: #1d4ed8;
  --color-pw-strong: #15803d;
}
JavaScript
// Password rules, strength meter and confirm-match. No libraries. The level counts the rules met, so swap in a
// proper estimator if you need one. Live text waits until typing pauses so screen readers aren't flooded.
(function () {
  var RULES = {
    len12: function (v) { return v.length >= 12; },
    len10: function (v) { return v.length >= 10; },
    case: function (v) { return /[a-z]/.test(v) && /[A-Z]/.test(v); },
    number: function (v) { return /\d/.test(v); },
    symbol: function (v) { return /[^A-Za-z0-9]/.test(v); }
  };
  var WORDS = ['none', 'weak', 'fair', 'good', 'strong'];
  document.querySelectorAll('[data-strength]').forEach(function (box) {
    var input = document.getElementById(box.dataset.strength), rules = box.querySelectorAll('[data-rule]');
    var meter = box.querySelector('[data-meter]'), word = box.querySelector('[data-word]'), live = box.querySelector('[data-live]'), timer;
    box.querySelector('[data-show]').hidden = false;
    input.addEventListener('input', function () {
      var v = input.value, met = 0;
      rules.forEach(function (li) {
        var ok = RULES[li.dataset.rule](v);
        li.dataset.met = ok; li.querySelector('[data-sr]').textContent = ok ? ' (done)' : '';
        if (ok) met++;
      });
      var level = v ? Math.max(1, Math.round(met / rules.length * 4)) : 0;
      meter.dataset.level = level; word.textContent = WORDS[level];
      clearTimeout(timer);
      timer = setTimeout(function () {
        live.textContent = v ? 'Strength ' + WORDS[level] + ', ' + met + ' of ' + rules.length + ' requirements met.' : '';
      }, 900);
    });
  });
  document.querySelectorAll('[data-match]').forEach(function (confirm) {
    var first = document.getElementById(confirm.dataset.match), out = document.getElementById(confirm.id + '-msg');
    function update() {
      var same = confirm.value === first.value, msg = !confirm.value ? '' : same ? 'Passwords match.' : 'Passwords do not match yet.';
      confirm.setCustomValidity(confirm.value && !same ? 'The passwords do not match.' : '');
      if (out.textContent !== msg) { out.textContent = msg; out.dataset.state = !msg ? '' : same ? 'ok' : 'no'; }
    }
    confirm.addEventListener('input', update); first.addEventListener('input', update);
  });
  document.querySelectorAll('[data-pw-toggle]').forEach(function (btn) {
    var input = document.getElementById(btn.getAttribute('aria-controls'));
    btn.hidden = false;
    btn.addEventListener('click', function () {
      var show = input.type === 'password';
      input.type = show ? 'text' : 'password';
      btn.setAttribute('aria-pressed', show ? 'true' : 'false');
    });
  });
  // the browser validates first; submit only fires once every rule passes. Demo only: nothing is sent.
  document.querySelectorAll('[data-pw-form]').forEach(function (form) {
    form.addEventListener('submit', function (e) {
      e.preventDefault();
      form.querySelector('[data-status]').textContent = 'Password saved. You can sign in now.';
    });
  });
})();

About this set

Two sign-up forms built around choosing a good password. Segmented meter and checklist is a password manager sign-up: an email field and a master password with a show and hide button, a four-segment strength meter with a Strength label, and a two-column checklist of rules (at least 12 characters, upper and lower case, a number, a symbol) whose circles fill with a tick as each rule is met. Strength bar and confirm field is a team invite: an invite banner, a new password with a continuous bar and three rule chips, then a confirm field that says whether the two passwords match. Use the first where the password protects something important, the second for invited users setting a password for the first time. The rules are plain text linked to the field with aria-describedby, and the input’s pattern and minlength enforce the same rules, so the form works as an ordinary form without JavaScript. The script ticks rules, sets the meter level and announces a short summary such as Strength good, 3 of 4 requirements met through a polite live region once typing pauses. The strength level counts the rules met; swap in a dedicated estimator if you need one.

What’s included

  • Segmented meter and continuous bar, both driven by a data-level attribute
  • Live requirement checklist and rule chips with hidden done text
  • Confirm-password match message using setCustomValidity
  • Polite, debounced live summary so screen readers are not flooded
  • pattern and minlength enforce the rules when JavaScript is off

Accessibility

Labels say (required) in words, and the rule list is linked to the password with aria-describedby so it is read as the field's description. Met rules gain a visually hidden (done) and a short summary is announced through an aria-live="polite" region about a second after typing stops; the match message is its own polite region that only updates when the state changes. The visual meter and its label are aria-hidden because the live summary carries the same information. The show password button uses aria-pressed, focus rings are visible, and transitions switch off under prefers-reduced-motion.

Customise it

Change --pw-accent for the brand and --pw-weak, --pw-fair, --pw-good and --pw-strong for the meter colours on .pw. The rules live in the RULES object in the script and in data-rule on each list item; keep the input's pattern in step with them. In Tailwind the colours are pw-* theme variables.