/* 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.';
});
});
})();
// 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.';
});
});
})();
// 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.