<form class="pw" novalidate>
<label for="new-password" class="pw-label">New password</label>
<div class="pw-field">
<input id="new-password" type="password" autocomplete="new-password" aria-describedby="pw-rules pw-summary">
<button class="pw-toggle" type="button" aria-pressed="false">Show</button>
</div>
<div class="pw-meter" aria-hidden="true"><span></span></div>
<p id="pw-summary" class="pw-summary" aria-live="polite">Strength: none</p>
<ul id="pw-rules" class="pw-rules">
<li data-rule="length">At least 12 characters</li>
<li data-rule="mixedCase">Upper and lower case letters</li>
<li data-rule="number">A number</li>
<li data-rule="symbol">A symbol, like ! or #</li>
</ul>
</form>
.pw { width: min(340px, 100%); display: grid; gap: 8px; font: 14px system-ui, sans-serif; }
.pw-label { font-weight: 600; }
.pw-field { display: flex; gap: 6px; padding: 4px; border-radius: 12px; background: var(--surface); box-shadow: inset 0 0 0 1px var(--line); }
.pw-field:focus-within { box-shadow: inset 0 0 0 2px var(--brand); }
.pw-field input { flex: 1; min-width: 0; border: 0; outline: 0; padding: 0 10px; font: 16px system-ui, sans-serif; color: var(--fg); background: transparent; }
.pw-toggle { border: 0; cursor: pointer; padding: 8px 12px; border-radius: 9px; font: 600 13px system-ui, sans-serif; color: var(--fg); background: var(--surface-2); }
/* The bar width and color come from --strength (0 to 4), set by JavaScript. */
.pw-meter { height: 6px; border-radius: 99px; background: var(--surface-2); overflow: hidden; }
.pw-meter span {
display: block;
height: 100%;
width: calc(var(--strength, 0) * 25%);
background: var(--meter-color, #c93a3f);
transition: width 0.3s ease, background 0.3s;
}
.pw-summary { margin: 0; color: var(--muted); }
.pw-rules { margin: 4px 0 0; padding: 0; list-style: none; display: grid; gap: 4px; color: var(--muted); }
.pw-rules li::before { content: '○'; display: inline-block; width: 1.4em; }
.pw-rules li.met { color: var(--fg); }
.pw-rules li.met::before { content: '✓'; color: #12a150; }
// Password strength meter: checks four simple rules as you type and fills a
// bar by how many pass. It explains *why* a password is weak, not just that it is.
const passwordInput = document.getElementById('new-password');
const toggleButton = document.querySelector('.pw-toggle');
const meter = document.querySelector('.pw-meter');
const summary = document.querySelector('.pw-summary');
const rules = {
length: (password) => password.length >= 12,
mixedCase: (password) => /[a-z]/.test(password) && /[A-Z]/.test(password),
number: (password) => /\d/.test(password),
symbol: (password) => /[^A-Za-z0-9]/.test(password),
};
const levels = [
{ name: 'none', color: '#c93a3f' },
{ name: 'weak', color: '#c93a3f' },
{ name: 'fair', color: '#e08a1e' },
{ name: 'good', color: '#3684ff' },
{ name: 'strong', color: '#12a150' },
];
function updateMeter() {
const password = passwordInput.value;
let passedRules = 0;
for (const [ruleName, check] of Object.entries(rules)) {
const isMet = check(password);
document.querySelector(`[data-rule="${ruleName}"]`).classList.toggle('met', isMet);
if (isMet) passedRules += 1;
}
// Any password counts as at least "weak", so the bar never looks empty while typing.
const score = password ? Math.max(1, passedRules) : 0;
const level = levels[score];
meter.style.setProperty('--strength', score);
meter.style.setProperty('--meter-color', level.color);
summary.textContent = `Strength: ${level.name}`;
}
passwordInput.addEventListener('input', updateMeter);
// Show/hide: switch the input type and keep the cursor where it was.
toggleButton.addEventListener('click', () => {
const isHidden = passwordInput.type === 'password';
passwordInput.type = isHidden ? 'text' : 'password';
toggleButton.textContent = isHidden ? 'Hide' : 'Show';
toggleButton.setAttribute('aria-pressed', String(isHidden));
passwordInput.focus();
});