Forms

PasswordStrength

A new-password field with a meter that fills as it gets stronger and a checklist of rules ticked as they are met.

#forms#input#security#feedback#interactive

Demo

Preview

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

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.qwe
main.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>}
Thread
54Useful? Like it, or add to its thread below.

Thread 0 posts

On PasswordStrength: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.