PasswordStrength
A new-password field with a meter that fills as it gets stronger and a checklist of rules ticked as they are met.
Demo
Strength: none yet
- At least 8 characters: not yet
- Upper and lower case letters: not yet
- A number: not yet
- A symbol, such as ! or #: not yet
Use a passphrase you do not use elsewhere.
Strength: none yet
- At least 12 characters: not yet
- Upper and lower case letters: not yet
- A number: not yet
Nothing typed.
Strength: weak
- At least 8 characters: not yet
- Upper and lower case letters: not yet
- A number: not yet
- A symbol, such as ! or #: not yet
That password was found in a leak.
Get it
It needs theme.qwe, as every component does; or take the whole kit, quillo-ui.qwe. Each component brings its own styles to the pages that show it.
$ curl -O https://quillolang.org/components/ui/theme.qwe$ curl -O https://quillolang.org/components/ui/password_strength.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/password_strength.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
demos/password_strength.qwe
component PasswordStrengthDemoLive() { state word = "" <div.demo-stack> <PasswordStrength name="pw_live" label="Choose a password" min_length={12} rules={["length", "case", "number"]} field="password-strength-demo-live" on_input={(t, s) => { word = s }}/> <p.demo-note>{word == "" ? "Nothing typed." : `on_input says ${word}.`}</p> </div>}view PasswordStrengthDemo() = <div.demo-stack.demo-narrow> <PasswordStrength name="pw" field="password-strength-demo-plain" hint="Use a passphrase you do not use elsewhere." required={true}/> <PasswordStrengthDemoLive/> <PasswordStrength name="pw_bad" label="Small" size="sm" field="password-strength-demo-small" value="abc" error="That password was found in a leak."/> </div>Source
ui/password_strength.qwe
// PasswordStrength: a password field for choosing a new password, with a meter// that fills as it gets stronger and a checklist of the rules it must meet, each// ticked as it is. It is a real <input type="password">, so a plain <form> sends// it; the rules not met keep the field invalid, so the form will not send a weak// one. A button shows what was typed.//// <PasswordStrength name="password" label="New password"/>// <PasswordStrength name="password" min_length={12} rules={["length", "case", "number"]}// on_input={(text, strength) => { ok = strength == "strong" }}/>//// min_length: the least characters (8). rules: which to ask for, of "length"// (at least min_length), "case" (upper and lower case letters), "number" (a// digit) and "symbol" (another character); all four by default. The meter says// weak, fair, good or strong: one bar per rule met, strong when all are, and it is// told to screen readers when it changes. value, placeholder, hint, error,// required, disabled, field (the id, default "password-strength-" + name) and// size ("sm" or "md") as for every field; autocomplete is "new-password".// on_input(text, strength): inside one of your components, called with the text// as it is typed and its strength word ("" for nothing typed).import ui/theme.qwe use ui_clslet password_strength_css = css`.ui-password-strength { display: grid; gap: 8px; font: 14px/1.4 var(--ui-font); color: var(--ui-fg); }.ui-password-strength-label { font-size: 13px; font-weight: 500; color: var(--ui-fg); }.ui-password-strength-req { margin-left: 2px; color: var(--ui-danger); }.ui-password-strength-box { position: relative; display: flex; align-items: center; }.ui-password-strength-control { box-sizing: border-box; width: 100%; height: 38px; margin: 0; padding: 0 44px 0 12px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm); background: var(--ui-surface); color: var(--ui-fg); font: 14px/1 var(--ui-font); transition: border-color .15s, box-shadow .15s;}.ui-password-strength-control::placeholder { color: var(--ui-muted); opacity: 1; }.ui-password-strength-control::-ms-reveal { display: none; }.ui-password-strength-control:hover { border-color: var(--ui-strong); }.ui-password-strength-control:focus { outline: none; border-color: var(--ui-accent); box-shadow: var(--ui-focus); }.ui-password-strength-control[aria-invalid="true"] { border-color: var(--ui-danger); }.ui-password-strength-control:disabled { opacity: .55; cursor: not-allowed; }.ui-password-strength-toggle { position: absolute; right: 5px; display: inline-grid; place-items: center; width: 30px; height: 28px; padding: 0; border: 0; border-radius: var(--ui-radius-sm); background: transparent; color: var(--ui-muted); cursor: pointer;}.ui-password-strength-toggle svg { width: 18px; height: 18px; }.ui-password-strength-toggle:hover { background: var(--ui-subtle); color: var(--ui-fg); }.ui-password-strength-toggle:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-password-strength-meter { display: flex; align-items: center; gap: 10px; margin: 0; font-size: 13px; color: var(--ui-muted); }.ui-password-strength-bars { display: grid; grid-auto-flow: column; grid-auto-columns: 1fr; gap: 4px; flex: 1; max-width: 220px; }.ui-password-strength-bars span { height: 5px; border-radius: 3px; background: var(--ui-border); transition: background .2s; }.ui-password-strength-weak .ui-password-strength-bars span:nth-child(-n+1) { background: var(--ui-danger); }.ui-password-strength-fair .ui-password-strength-bars span:nth-child(-n+2) { background: var(--ui-warning); }.ui-password-strength-good .ui-password-strength-bars span:nth-child(-n+3) { background: var(--ui-info); }.ui-password-strength-strong .ui-password-strength-bars span { background: var(--ui-success); }.ui-password-strength-weak .ui-password-strength-word { color: var(--ui-danger); }.ui-password-strength-fair .ui-password-strength-word { color: var(--ui-warning); }.ui-password-strength-good .ui-password-strength-word { color: var(--ui-info); }.ui-password-strength-strong .ui-password-strength-word { color: var(--ui-success); }.ui-password-strength-rules { display: grid; gap: 4px; margin: 0; padding: 0; list-style: none; font-size: 13px; color: var(--ui-muted); }.ui-password-strength-rule { display: flex; align-items: center; gap: 8px; }.ui-password-strength-rule svg { flex: none; width: 14px; height: 14px; }.ui-password-strength-met { color: var(--ui-success); }.ui-password-strength-sm .ui-password-strength-control { height: 30px; font-size: 13px; padding-right: 38px; }.ui-password-strength-sm .ui-password-strength-toggle { right: 3px; height: 24px; width: 28px; }.ui-password-strength-hint, .ui-password-strength-error { margin: 0; font-size: 13px; color: var(--ui-muted); }.ui-password-strength-error { color: var(--ui-danger); }@media (prefers-reduced-motion: reduce) { .ui-password-strength-control, .ui-password-strength-bars span { transition: none; } }`let password_strength_rules = ["length", "case", "number", "symbol"]// What a rule asks for.fn password_strength_rule_text(rule: str, min_length: int) -> str = match rule { "length" => `At least ${min_length} characters` "case" => "Upper and lower case letters" "number" => "A number" "symbol" => "A symbol, such as ! or #" _ => rule}// Whether the text meets a rule.fn password_strength_meets(rule: str, text: str, min_length: int) -> bool = match rule { "length" => len(text) >= min_length "case" => re.test(text, "[a-z]") && re.test(text, "[A-Z]") "number" => re.test(text, "\\d") "symbol" => re.test(text, "[^A-Za-z0-9]") _ => true}// "weak", "fair", "good" or "strong" from how many of the rules are met ("" for nothing typed).fn password_strength_word(met: int, total: int, text: str) -> str { if len(text) == 0 { return "" } if met >= total { return "strong" } let share = total == 0 ? 1 : met / total share >= 0.75 ? "good" : share >= 0.5 ? "fair" : "weak"}component PasswordStrength(name: str = "password", label: str = "New password", value: str = "", min_length: int = 8, rules: list = password_strength_rules, placeholder: str? = null, hint: str? = null, error: str? = null, required: bool = false, disabled: bool = false, field: str? = null, size: str = "md", on_input = null, class: str? = null) { style global password_strength_css state text = value state shown = false let id = field ?? `password-strength-${name}` let met = rules | filter((r) => password_strength_meets(r, text, min_length)) let strength = password_strength_word(len(met), len(rules), text) let unmet = rules | filter((r) => !password_strength_meets(r, text, min_length)) let described = ui_cls([hint != null && `${id}-hint`, `${id}-meter`, `${id}-rules`, error != null && `${id}-error`]) <div class={ui_cls(["ui-password-strength", size == "sm" && "ui-password-strength-sm", class])}> <label.ui-password-strength-label for={id}> {label}{required && <span.ui-password-strength-req aria-hidden="true">*</span>} </label> <div.ui-password-strength-box> <input.ui-password-strength-control type={shown ? "text" : "password"} id={id} name={name} value={text} placeholder={placeholder} required={required} disabled={disabled} minlength={contains(rules, "length") ? min_length : null} autocomplete="new-password" autocapitalize="off" spellcheck="false" aria-invalid={error != null ? "true" : null} aria-describedby={described} @input={(e) => { text = e.value let left = rules | filter((r) => !password_strength_meets(r, e.value, min_length)) e.target.setCustomValidity(len(left) > 0 && len(e.value) > 0 ? `Needs: ${join(left | map((r) => lower(password_strength_rule_text(r, min_length))), "; ")}` : "") on_input && on_input(e.value, password_strength_word(len(rules) - len(left), len(rules), e.value)) }}/> <button.ui-password-strength-toggle type="button" aria-controls={id} aria-pressed={shown ? "true" : "false"} aria-label={shown ? "Hide password" : "Show password"} title={shown ? "Hide password" : "Show password"} disabled={disabled} @click={() => { shown = !shown }}> {shown ? <svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 3l14 14"/><path d="M8.2 4.6A8.6 8.6 0 0 1 10 4.4c4.4 0 7.4 3.9 8.2 5.6-.4.8-1.2 2-2.4 3.1M12.8 15a7.9 7.9 0 0 1-2.8.6c-4.4 0-7.4-3.9-8.2-5.6.5-1 1.6-2.6 3.2-3.8"/><path d="M8.4 8.4a2.3 2.3 0 0 0 3.2 3.2"/></svg> : <svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M1.8 10c.8-1.7 3.8-5.6 8.2-5.6s7.4 3.9 8.2 5.6c-.8 1.7-3.8 5.6-8.2 5.6S2.6 11.7 1.8 10z"/><circle cx="10" cy="10" r="2.6"/></svg>} </button> </div> <p class={ui_cls(["ui-password-strength-meter", strength != "" && `ui-password-strength-${strength}`])} id={`${id}-meter`} aria-live="polite"> <span.ui-password-strength-bars aria-hidden="true">{for r in rules => <span key={r}></span>}</span> <span.ui-password-strength-word>{strength != "" ? `Strength: ${strength}` : "Strength: none yet"}</span> </p> <ul.ui-password-strength-rules id={`${id}-rules`} aria-label="Password rules"> {for r in rules => { let ok = !contains(unmet, r) <li key={r} class={ui_cls(["ui-password-strength-rule", ok && "ui-password-strength-met"])} data-met={ok ? "true" : "false"}> {ok ? <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3.5 8.5l3 3 6-7"/></svg> : <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><circle cx="8" cy="8" r="5.5"/></svg>} <span>{password_strength_rule_text(r, min_length)}<span.ui-sr>{ok ? ": met" : ": not yet"}</span></span> </li> }} </ul> {hint != null && <p.ui-password-strength-hint id={`${id}-hint`}>{hint}</p>} {error != null && <p.ui-password-strength-error id={`${id}-error`} role="alert">{error}</p>} </div>}Related
ConsentCheckboxA required checkbox beside legal text with links to the terms and the privacy policy, posted as a real checkbox.5 views · 6 downloadsNpsInputThe eleven scores 0 to 10 of a Net Promoter question in a row, with the words at the ends: real radios posted with a form.5 views · 7 downloadsOtpInputBoxes for a one-time code: typing moves along, Backspace goes back, pasting fills them all.5 views · 5 downloadsPasswordInputA password field with a show/hide toggle and, for new passwords, a strength meter.5 views · 4 downloadsAddressFormStreet, city, region, postcode and country in the usual layout, with the autocomplete names browsers fill from.5 views · 7 downloadsAuthFormSign in, sign up, forgot and reset password in one form that switches in place, each posting to its own route.7 views · 4 downloads
Thread
Thread 0 posts
On PasswordStrength: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.