Form — Password strength + rules checklist
A set-password field that grades strength in real time with a color-coded four-segment meter while a live requirements checklist ticks off minimum length, uppercase, lowercase, number, and symbol as you type. A show/hide toggle reveals the value, a confirm-password field reports match state, and the submit button stays disabled until the password is strong and both entries agree. Strength is announced politely for screen readers.
MCP
Code
:root {
--brand: #5b5bf0;
--brand-d: #4646d6;
--brand-700: #3a3ab8;
--brand-50: #eef0ff;
--accent: #00b4a6;
--accent-soft: #d8f5f2;
--ink: #101322;
--ink-2: #3a4060;
--muted: #6c7393;
--bg: #f6f7fb;
--white: #ffffff;
--surface: #ffffff;
--line: rgba(16, 19, 34, 0.1);
--line-2: rgba(16, 19, 34, 0.16);
--ok: #2f9e6f;
--warn: #d98a2b;
--danger: #d4503e;
--r-sm: 8px;
--r-md: 14px;
--r-lg: 20px;
--sh-1: 0 1px 2px rgba(16, 19, 34, 0.08);
--sh-2: 0 8px 24px rgba(16, 19, 34, 0.08);
}
*,
*::before,
*::after {
box-sizing: border-box;
}
html {
-webkit-text-size-adjust: 100%;
}
body {
margin: 0;
font-family: "Inter", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
line-height: 1.5;
color: var(--ink);
background:
radial-gradient(1100px 540px at 100% -10%, rgba(91, 91, 240, 0.08), transparent 60%),
radial-gradient(900px 480px at -10% 110%, rgba(0, 180, 166, 0.07), transparent 55%),
var(--bg);
min-height: 100vh;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0 0 0 0);
white-space: nowrap;
border: 0;
}
/* Layout ------------------------------------------------------------- */
.page {
display: flex;
align-items: flex-start;
justify-content: center;
min-height: 100vh;
padding: clamp(20px, 5vw, 56px) 18px;
}
.card {
width: 100%;
max-width: 460px;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
box-shadow: var(--sh-2);
padding: clamp(22px, 4vw, 32px);
}
/* Head --------------------------------------------------------------- */
.card-head {
margin-bottom: 22px;
}
.badge {
display: inline-flex;
align-items: center;
gap: 7px;
font-size: 12px;
font-weight: 600;
letter-spacing: 0.02em;
color: var(--brand-700);
background: var(--brand-50);
border: 1px solid rgba(91, 91, 240, 0.18);
padding: 5px 11px;
border-radius: 999px;
}
.badge svg {
width: 14px;
height: 14px;
}
.title {
margin: 14px 0 6px;
font-size: clamp(20px, 3.5vw, 24px);
font-weight: 800;
letter-spacing: -0.02em;
}
.subtitle {
margin: 0;
font-size: 14px;
color: var(--muted);
}
.subtitle strong {
color: var(--ink-2);
font-weight: 600;
}
/* Field -------------------------------------------------------------- */
.field {
margin-top: 20px;
}
.label {
display: block;
font-size: 13px;
font-weight: 600;
color: var(--ink-2);
margin-bottom: 7px;
}
.req {
color: var(--danger);
font-weight: 700;
}
.input-shell {
position: relative;
display: flex;
align-items: center;
}
.input-ico {
position: absolute;
left: 13px;
display: inline-flex;
color: var(--muted);
pointer-events: none;
}
.input-ico svg {
width: 18px;
height: 18px;
}
.input {
width: 100%;
font: inherit;
font-size: 15px;
color: var(--ink);
background: var(--white);
border: 1.5px solid var(--line-2);
border-radius: var(--r-md);
padding: 12px 46px 12px 40px;
transition: border-color 0.16s ease, box-shadow 0.16s ease, background 0.16s ease;
}
.input::placeholder {
color: #a4a9c2;
}
.input:hover {
border-color: rgba(16, 19, 34, 0.26);
}
.input:focus-visible {
outline: none;
border-color: var(--brand);
box-shadow: 0 0 0 4px rgba(91, 91, 240, 0.16);
}
.field.is-error .input {
border-color: var(--danger);
background: #fdf5f4;
}
.field.is-error .input:focus-visible {
box-shadow: 0 0 0 4px rgba(212, 80, 62, 0.16);
}
.field.is-success .input {
border-color: var(--ok);
}
.field.is-success .input:focus-visible {
box-shadow: 0 0 0 4px rgba(47, 158, 111, 0.16);
}
.field.is-success .input-ico {
color: var(--ok);
}
/* Show / hide toggle ------------------------------------------------- */
.toggle {
position: absolute;
right: 6px;
display: inline-flex;
align-items: center;
justify-content: center;
width: 34px;
height: 34px;
border: none;
background: transparent;
color: var(--muted);
border-radius: var(--r-sm);
cursor: pointer;
transition: color 0.15s ease, background 0.15s ease;
}
.toggle:hover {
color: var(--ink-2);
background: rgba(16, 19, 34, 0.05);
}
.toggle:focus-visible {
outline: none;
color: var(--brand-d);
box-shadow: 0 0 0 3px rgba(91, 91, 240, 0.2);
}
.toggle svg {
width: 19px;
height: 19px;
}
/* Strength meter ----------------------------------------------------- */
.meter {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 6px;
margin: 14px 0 8px;
}
.meter-seg {
height: 6px;
border-radius: 999px;
background: rgba(16, 19, 34, 0.09);
transition: background 0.28s ease;
}
.meter[data-level="1"] .meter-seg:nth-child(-n + 1),
.meter[data-level="2"] .meter-seg:nth-child(-n + 2),
.meter[data-level="3"] .meter-seg:nth-child(-n + 3),
.meter[data-level="4"] .meter-seg:nth-child(-n + 4) {
background: var(--seg, var(--muted));
}
.meter[data-level="1"] {
--seg: var(--danger);
}
.meter[data-level="2"] {
--seg: var(--warn);
}
.meter[data-level="3"] {
--seg: #4f9bd6;
}
.meter[data-level="4"] {
--seg: var(--ok);
}
.meter-label {
display: flex;
align-items: center;
gap: 8px;
margin: 0 0 4px;
font-size: 12.5px;
font-weight: 600;
color: var(--muted);
}
.meter-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: rgba(16, 19, 34, 0.2);
transition: background 0.2s ease;
flex: none;
}
.meter-label[data-level="1"] {
color: var(--danger);
}
.meter-label[data-level="1"] .meter-dot {
background: var(--danger);
}
.meter-label[data-level="2"] {
color: var(--warn);
}
.meter-label[data-level="2"] .meter-dot {
background: var(--warn);
}
.meter-label[data-level="3"] {
color: #3a7fb5;
}
.meter-label[data-level="3"] .meter-dot {
background: #4f9bd6;
}
.meter-label[data-level="4"] {
color: var(--ok);
}
.meter-label[data-level="4"] .meter-dot {
background: var(--ok);
}
/* Rules checklist ---------------------------------------------------- */
.rules {
list-style: none;
margin: 12px 0 0;
padding: 14px;
display: grid;
gap: 9px;
background: #fafbff;
border: 1px solid var(--line);
border-radius: var(--r-md);
}
.rule {
display: flex;
align-items: center;
gap: 9px;
font-size: 13px;
color: var(--muted);
transition: color 0.18s ease;
}
.rule-mark {
position: relative;
flex: none;
width: 18px;
height: 18px;
border-radius: 50%;
border: 1.5px solid var(--line-2);
background: var(--white);
transition: background 0.18s ease, border-color 0.18s ease, transform 0.18s ease;
}
.rule-mark::after {
content: "";
position: absolute;
left: 5px;
top: 2px;
width: 5px;
height: 9px;
border: solid var(--white);
border-width: 0 2px 2px 0;
transform: rotate(45deg) scale(0);
transition: transform 0.18s ease;
}
.rule.met {
color: var(--ink-2);
font-weight: 500;
}
.rule.met .rule-mark {
background: var(--ok);
border-color: var(--ok);
transform: scale(1.05);
}
.rule.met .rule-mark::after {
transform: rotate(45deg) scale(1);
}
/* Help / error text -------------------------------------------------- */
.help {
margin: 9px 0 0;
font-size: 12.5px;
color: var(--muted);
}
.help.is-error {
color: var(--danger);
font-weight: 500;
}
.help.is-ok {
color: var(--ok);
font-weight: 500;
}
/* Submit ------------------------------------------------------------- */
.submit {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
width: 100%;
margin-top: 24px;
font: inherit;
font-size: 15px;
font-weight: 700;
color: var(--white);
background: var(--brand);
border: none;
border-radius: var(--r-md);
padding: 13px 18px;
cursor: pointer;
box-shadow: var(--sh-1);
transition: background 0.16s ease, transform 0.08s ease, box-shadow 0.16s ease, opacity 0.16s ease;
}
.submit:hover:not(:disabled) {
background: var(--brand-d);
}
.submit:active:not(:disabled) {
transform: translateY(1px);
}
.submit:focus-visible {
outline: none;
box-shadow: 0 0 0 4px rgba(91, 91, 240, 0.28);
}
.submit:disabled {
background: #c9cbe6;
color: #f3f3fb;
cursor: not-allowed;
box-shadow: none;
}
.submit-ico {
width: 18px;
height: 18px;
transition: transform 0.16s ease;
}
.submit:hover:not(:disabled) .submit-ico {
transform: translateX(3px);
}
.submit.ghost {
background: var(--white);
color: var(--brand-d);
border: 1.5px solid var(--line-2);
}
.submit.ghost:hover {
background: var(--brand-50);
border-color: rgba(91, 91, 240, 0.3);
}
/* Success card ------------------------------------------------------- */
.success-card {
text-align: center;
animation: pop 0.4s ease;
}
.success-ring {
display: inline-flex;
align-items: center;
justify-content: center;
width: 64px;
height: 64px;
border-radius: 50%;
color: var(--white);
background: var(--ok);
box-shadow: 0 8px 22px rgba(47, 158, 111, 0.3);
margin-bottom: 16px;
}
.success-ring svg {
width: 32px;
height: 32px;
}
@keyframes pop {
from {
opacity: 0;
transform: translateY(8px) scale(0.98);
}
to {
opacity: 1;
transform: none;
}
}
/* Toast -------------------------------------------------------------- */
.toast-wrap {
position: fixed;
left: 50%;
bottom: 24px;
transform: translateX(-50%);
display: flex;
flex-direction: column;
gap: 10px;
z-index: 50;
pointer-events: none;
}
.toast {
display: flex;
align-items: center;
gap: 9px;
background: var(--ink);
color: var(--white);
font-size: 13.5px;
font-weight: 500;
padding: 11px 16px;
border-radius: var(--r-md);
box-shadow: var(--sh-2);
opacity: 0;
transform: translateY(10px);
transition: opacity 0.22s ease, transform 0.22s ease;
}
.toast.show {
opacity: 1;
transform: none;
}
.toast svg {
width: 17px;
height: 17px;
flex: none;
color: var(--accent);
}
/* Responsive --------------------------------------------------------- */
@media (max-width: 520px) {
.page {
align-items: stretch;
padding: 16px 12px;
}
.card {
border-radius: var(--r-md);
padding: 20px;
}
.meter {
gap: 5px;
}
.rules {
padding: 12px;
}
.toast-wrap {
left: 12px;
right: 12px;
transform: none;
}
.toast {
width: 100%;
}
}
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.001ms !important;
transition-duration: 0.001ms !important;
}
}(function () {
"use strict";
var form = document.getElementById("pw-form");
var pw = document.getElementById("pw");
var confirm = document.getElementById("confirm");
var submitBtn = document.getElementById("submit-btn");
var meter = document.querySelector(".meter");
var meterLabel = document.getElementById("meter-label");
var meterText = meterLabel.querySelector(".meter-text");
var rulesList = document.getElementById("rules-list");
var ruleEls = {};
rulesList.querySelectorAll(".rule").forEach(function (el) {
ruleEls[el.dataset.rule] = el;
});
var fieldPw = document.getElementById("field-pw");
var fieldConfirm = document.getElementById("field-confirm");
var confirmHelp = document.getElementById("confirm-help");
var successCard = document.getElementById("success-card");
var resetBtn = document.getElementById("reset-btn");
var toastWrap = document.getElementById("toast-wrap");
// Strength label per level (0 = none, 4 = strong)
var LEVELS = ["Too weak", "Weak", "Fair", "Good", "Strong"];
/* ---- toast helper ------------------------------------------------ */
function toast(msg) {
var el = document.createElement("div");
el.className = "toast";
el.setAttribute("role", "status");
el.innerHTML =
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6 9 17l-5-5"/></svg>';
el.appendChild(document.createTextNode(msg));
toastWrap.appendChild(el);
requestAnimationFrame(function () {
el.classList.add("show");
});
setTimeout(function () {
el.classList.remove("show");
setTimeout(function () {
el.remove();
}, 260);
}, 3200);
}
/* ---- rule checks ------------------------------------------------- */
function checkRules(value) {
return {
len: value.length >= 10,
upper: /[A-Z]/.test(value),
lower: /[a-z]/.test(value),
number: /[0-9]/.test(value),
symbol: /[^A-Za-z0-9]/.test(value),
};
}
// Count met rules → score 0..5, mapped to level 0..4.
function levelFromRules(rules) {
var passed = 0;
Object.keys(rules).forEach(function (k) {
if (rules[k]) passed++;
});
// Need all 5 rules to reach "Strong".
if (passed <= 2) return 1; // Weak
if (passed === 3) return 2; // Fair
if (passed === 4) return 3; // Good
return 4; // Strong (all 5)
}
function allRulesMet(rules) {
return rules.len && rules.upper && rules.lower && rules.number && rules.symbol;
}
/* ---- render ------------------------------------------------------ */
function renderMeter(value, rules) {
if (!value) {
meter.removeAttribute("data-level");
meterLabel.removeAttribute("data-level");
meterText.textContent = "Start typing to check strength";
return 0;
}
var level = levelFromRules(rules);
meter.setAttribute("data-level", String(level));
meterLabel.setAttribute("data-level", String(level));
meterText.textContent = LEVELS[level] + " password";
return level;
}
function renderRules(rules) {
Object.keys(rules).forEach(function (key) {
var el = ruleEls[key];
if (!el) return;
el.classList.toggle("met", rules[key]);
});
}
/* ---- confirm match ---------------------------------------------- */
function renderConfirm(pwVal, confirmVal, strong) {
// Nothing typed yet in confirm.
if (!confirmVal) {
fieldConfirm.classList.remove("is-error", "is-success");
confirm.removeAttribute("aria-invalid");
confirmHelp.className = "help";
confirmHelp.textContent = "Both passwords must match.";
return false;
}
var match = pwVal === confirmVal && pwVal.length > 0;
if (match) {
fieldConfirm.classList.remove("is-error");
fieldConfirm.classList.add("is-success");
confirm.setAttribute("aria-invalid", "false");
confirmHelp.className = "help is-ok";
confirmHelp.textContent = strong
? "Passwords match."
: "Passwords match — strengthen the password above to continue.";
} else {
fieldConfirm.classList.remove("is-success");
fieldConfirm.classList.add("is-error");
confirm.setAttribute("aria-invalid", "true");
confirmHelp.className = "help is-error";
confirmHelp.textContent = "Passwords do not match yet.";
}
return match;
}
/* ---- master update ----------------------------------------------- */
function update() {
var pwVal = pw.value;
var confirmVal = confirm.value;
var rules = checkRules(pwVal);
renderRules(rules);
renderMeter(pwVal, rules);
var strong = allRulesMet(rules);
var match = renderConfirm(pwVal, confirmVal, strong);
// Password field state for screen readers.
if (pwVal && !strong) {
pw.setAttribute("aria-invalid", "true");
fieldPw.classList.remove("is-success");
} else if (strong) {
pw.setAttribute("aria-invalid", "false");
fieldPw.classList.add("is-success");
fieldPw.classList.remove("is-error");
} else {
pw.removeAttribute("aria-invalid");
fieldPw.classList.remove("is-success", "is-error");
}
var canSubmit = strong && match;
submitBtn.disabled = !canSubmit;
submitBtn.setAttribute("aria-disabled", String(!canSubmit));
}
/* ---- show / hide toggles ---------------------------------------- */
function wireToggle(id, input) {
var btn = document.getElementById(id);
btn.addEventListener("click", function () {
var show = input.type === "password";
input.type = show ? "text" : "password";
btn.setAttribute("aria-pressed", String(show));
btn.setAttribute("aria-label", show ? "Hide password" : "Show password");
btn.querySelector(".ico-eye").hidden = show;
btn.querySelector(".ico-eye-off").hidden = !show;
input.focus();
});
}
wireToggle("pw-toggle", pw);
wireToggle("confirm-toggle", confirm);
/* ---- events ------------------------------------------------------ */
pw.addEventListener("input", update);
confirm.addEventListener("input", update);
// Validate confirm on blur even if empty after touching.
confirm.addEventListener("blur", function () {
if (!confirm.value && pw.value) {
fieldConfirm.classList.add("is-error");
confirm.setAttribute("aria-invalid", "true");
confirmHelp.className = "help is-error";
confirmHelp.textContent = "Please re-enter your password.";
}
});
form.addEventListener("submit", function (e) {
e.preventDefault();
var rules = checkRules(pw.value);
if (!allRulesMet(rules) || pw.value !== confirm.value) {
update();
toast("Fix the highlighted fields first.");
return;
}
// Success state.
form.hidden = true;
successCard.hidden = false;
successCard.focus && successCard.focus();
toast("Password created successfully.");
});
resetBtn.addEventListener("click", function () {
form.reset();
successCard.hidden = true;
form.hidden = false;
fieldPw.classList.remove("is-success", "is-error");
fieldConfirm.classList.remove("is-success", "is-error");
pw.removeAttribute("aria-invalid");
confirm.removeAttribute("aria-invalid");
confirmHelp.className = "help";
confirmHelp.textContent = "Both passwords must match.";
update();
pw.focus();
});
// Initial paint.
update();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Password strength + rules checklist</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<main class="page">
<form class="card" id="pw-form" novalidate>
<header class="card-head">
<span class="badge">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<rect x="3" y="11" width="18" height="11" rx="2" />
<path d="M7 11V7a5 5 0 0 1 10 0v4" />
</svg>
Account security
</span>
<h1 class="title">Set your password</h1>
<p class="subtitle">
Choose a strong password for <strong>[email protected]</strong>. We will
ask you to enter it twice to avoid typos.
</p>
</header>
<!-- New password -->
<div class="field" id="field-pw">
<label class="label" for="pw">
New password <span class="req" aria-hidden="true">*</span>
<span class="sr-only">required</span>
</label>
<div class="input-shell">
<span class="input-ico" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="11" width="18" height="11" rx="2" />
<path d="M7 11V7a5 5 0 0 1 10 0v4" />
</svg>
</span>
<input
id="pw"
name="password"
class="input"
type="password"
autocomplete="new-password"
spellcheck="false"
placeholder="Enter a password"
aria-required="true"
aria-describedby="pw-help meter-label rules-list"
/>
<button type="button" class="toggle" id="pw-toggle" aria-pressed="false" aria-controls="pw" aria-label="Show password">
<svg class="ico-eye" 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.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7Z" />
<circle cx="12" cy="12" r="3" />
</svg>
<svg class="ico-eye-off" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" hidden>
<path d="M9.9 4.24A9.1 9.1 0 0 1 12 4c6.5 0 10 8 10 8a17.6 17.6 0 0 1-2.16 3.19" />
<path d="M6.6 6.6A17.7 17.7 0 0 0 2 12s3.5 8 10 8a9.3 9.3 0 0 0 5.4-1.6" />
<path d="m9.5 9.5a3 3 0 0 0 4.2 4.2" />
<line x1="2" y1="2" x2="22" y2="22" />
</svg>
</button>
</div>
<!-- Strength meter -->
<div class="meter" aria-hidden="true">
<span class="meter-seg"></span>
<span class="meter-seg"></span>
<span class="meter-seg"></span>
<span class="meter-seg"></span>
</div>
<p class="meter-label" id="meter-label" aria-live="polite">
<span class="meter-dot" aria-hidden="true"></span>
<span class="meter-text">Start typing to check strength</span>
</p>
<!-- Rules checklist -->
<ul class="rules" id="rules-list" aria-label="Password requirements">
<li class="rule" data-rule="len">
<span class="rule-mark" aria-hidden="true"></span>
<span class="rule-text">At least 10 characters</span>
</li>
<li class="rule" data-rule="upper">
<span class="rule-mark" aria-hidden="true"></span>
<span class="rule-text">An uppercase letter (A–Z)</span>
</li>
<li class="rule" data-rule="lower">
<span class="rule-mark" aria-hidden="true"></span>
<span class="rule-text">A lowercase letter (a–z)</span>
</li>
<li class="rule" data-rule="number">
<span class="rule-mark" aria-hidden="true"></span>
<span class="rule-text">A number (0–9)</span>
</li>
<li class="rule" data-rule="symbol">
<span class="rule-mark" aria-hidden="true"></span>
<span class="rule-text">A symbol (!@#$…)</span>
</li>
</ul>
<p class="help" id="pw-help">Avoid names, common words, or sequences like 12345.</p>
</div>
<!-- Confirm password -->
<div class="field" id="field-confirm">
<label class="label" for="confirm">
Confirm password <span class="req" aria-hidden="true">*</span>
<span class="sr-only">required</span>
</label>
<div class="input-shell">
<span class="input-ico" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
<path d="M20 6 9 17l-5-5" />
</svg>
</span>
<input
id="confirm"
name="confirm"
class="input"
type="password"
autocomplete="new-password"
spellcheck="false"
placeholder="Re-enter the password"
aria-required="true"
aria-describedby="confirm-help"
/>
<button type="button" class="toggle" id="confirm-toggle" aria-pressed="false" aria-controls="confirm" aria-label="Show password">
<svg class="ico-eye" 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.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7Z" />
<circle cx="12" cy="12" r="3" />
</svg>
<svg class="ico-eye-off" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" hidden>
<path d="M9.9 4.24A9.1 9.1 0 0 1 12 4c6.5 0 10 8 10 8a17.6 17.6 0 0 1-2.16 3.19" />
<path d="M6.6 6.6A17.7 17.7 0 0 0 2 12s3.5 8 10 8a9.3 9.3 0 0 0 5.4-1.6" />
<path d="m9.5 9.5a3 3 0 0 0 4.2 4.2" />
<line x1="2" y1="2" x2="22" y2="22" />
</svg>
</button>
</div>
<p class="help" id="confirm-help" aria-live="polite">Both passwords must match.</p>
</div>
<button type="submit" class="submit" id="submit-btn" disabled aria-disabled="true">
<span class="submit-label">Create password</span>
<svg class="submit-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M5 12h14" />
<path d="m13 6 6 6-6 6" />
</svg>
</button>
</form>
<!-- Success state -->
<div class="card success-card" id="success-card" hidden>
<span class="success-ring" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
<path d="M20 6 9 17l-5-5" />
</svg>
</span>
<h2 class="title">Password updated</h2>
<p class="subtitle">Your new password is active. You can sign in with it from now on.</p>
<button type="button" class="submit ghost" id="reset-btn">Set another password</button>
</div>
</main>
<div class="toast-wrap" id="toast-wrap" aria-live="polite" aria-atomic="true"></div>
<script src="script.js"></script>
</body>
</html>Password strength + rules checklist
A self-contained set-password form that gives instant feedback as the user types. A four-segment strength meter shifts from red through amber and blue to green, and a label below it reads out “Too weak”, “Fair”, “Good”, or “Strong”. Underneath, a live requirements checklist ticks off each rule — at least 10 characters, an uppercase letter, a lowercase letter, a number, and a symbol — with a check mark animating in as each is satisfied.
Each password field has its own show/hide eye toggle that flips the input between obscured and plain text while keeping focus, and reports its pressed state through aria-pressed. The confirm field validates the match continuously: it goes green when the two entries agree and red with helper text when they diverge or are left empty after typing.
The submit button only enables when the password meets every rule and the confirmation matches, so there is no fake submit. On success the form swaps to a confirmation panel, and a small aria-live toast announces the result. Strength changes are announced politely, fields expose aria-invalid and aria-describedby, and the layout stacks cleanly down to about 360px.