PasswordInput
A password field with a show/hide toggle and, for new passwords, a strength meter.
Demo
Strength: none yet
At least 12 characters, with upper and lower case and a digit.
0 characters typed.
The passwords do not match.
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_input.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/password_input.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
demos/password_input.qwe
component PasswordInputDemoLive() { state pw = "" <div.demo-stack> <PasswordInput name="pw-new" label="New password" new={true} meter={true} hint="At least 12 characters, with upper and lower case and a digit." required={true} on_input={(t) => { pw = t }}/> <p.demo-note>{len(pw)} characters typed.</p> </div>}view PasswordInputDemo() = <div.demo-stack.demo-narrow> <PasswordInput name="pw-current" placeholder="Your password"/> <PasswordInputDemoLive/> <PasswordInput name="pw-bad" label="Confirm password" value="hunter2" error="The passwords do not match." size="sm"/> </div>Source
ui/password_input.qwe
// PasswordInput: a password field with a button that shows what was typed, and,// for a new password, a meter that says how strong it is.//// <PasswordInput/>// <PasswordInput name="new_password" label="New password" new={true} meter={true}// hint="At least 12 characters." required={true}/>//// new: a password being chosen, not entered: the browser offers to make one up// (autocomplete "new-password" rather than "current-password"; `autocomplete`// sets it outright). meter: shows weak, fair or strong as it is typed, from its// length and the kinds of characters in it (lower and upper case, digits, others),// and tells screen readers when that changes. The show button is a toggle// (aria-pressed), "Show password" or "Hide password".// name ("password"), label ("Password"), value, placeholder, hint, error, required,// disabled, field (the input's id, "password-input-" + name by default), size// ("sm" or "md") as for every field.// on_input(text): inside one of your components, called with the text as it is typed.import ui/theme.qwe use ui_clslet password_input_css = css`.ui-password-input { display: grid; gap: 6px; font: 14px/1.4 var(--ui-font); color: var(--ui-fg); }.ui-password-input-label { font-size: 13px; font-weight: 500; color: var(--ui-fg); }.ui-password-input-req { margin-left: 2px; color: var(--ui-danger); }.ui-password-input-hint { margin: 0; font-size: 13px; color: var(--ui-muted); }.ui-password-input-error { margin: 0; font-size: 13px; color: var(--ui-danger); }.ui-password-input-box { position: relative; display: flex; align-items: center; }.ui-password-input-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-input-control::placeholder { color: var(--ui-muted); opacity: 1; }.ui-password-input-control::-ms-reveal { display: none; }.ui-password-input-control:hover { border-color: var(--ui-strong); }.ui-password-input-control:focus { outline: none; border-color: var(--ui-accent); box-shadow: var(--ui-focus); }.ui-password-input-control[aria-invalid="true"] { border-color: var(--ui-danger); }.ui-password-input-control:disabled { opacity: .55; cursor: not-allowed; }.ui-password-input-toggle { position: absolute; right: 5px; display: inline-flex; align-items: center; justify-content: 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-input-toggle svg { width: 18px; height: 18px; }.ui-password-input-toggle:hover { background: var(--ui-subtle); color: var(--ui-fg); }.ui-password-input-toggle:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-password-input-toggle:disabled { opacity: .55; cursor: not-allowed; }.ui-password-input-sm .ui-password-input-control { height: 30px; font-size: 13px; padding-right: 38px; }.ui-password-input-sm .ui-password-input-toggle { right: 3px; height: 24px; width: 28px; }.ui-password-input-sm .ui-password-input-toggle svg { width: 16px; height: 16px; }.ui-password-input-meter { display: flex; align-items: center; gap: 10px; margin: 0; font-size: 13px; color: var(--ui-muted); }.ui-password-input-bars { display: grid; grid-template-columns: repeat(3, 1fr); gap: 4px; flex: 1; max-width: 180px; }.ui-password-input-bars span { height: 4px; border-radius: 2px; background: var(--ui-border); transition: background .2s; }.ui-password-input-weak .ui-password-input-bars span:nth-child(-n+1) { background: var(--ui-danger); }.ui-password-input-fair .ui-password-input-bars span:nth-child(-n+2) { background: var(--ui-warning); }.ui-password-input-strong .ui-password-input-bars span { background: var(--ui-success); }.ui-password-input-weak .ui-password-input-word { color: var(--ui-danger); }.ui-password-input-fair .ui-password-input-word { color: var(--ui-warning); }.ui-password-input-strong .ui-password-input-word { color: var(--ui-success); }@media (prefers-reduced-motion: reduce) { .ui-password-input-control, .ui-password-input-bars span { transition: none; } }`// "weak", "fair" or "strong" ("" for nothing typed): length and the kinds of characters.fn password_input_strength(text: str) -> str { let n = len(text) if n == 0 { return "" } let kinds = ["[a-z]", "[A-Z]", "\\d", "[^A-Za-z0-9]"] | filter((p) => re.test(text, p)) | len if (n >= 12 && kinds >= 3) || (n >= 16 && kinds >= 2) { return "strong" } if n >= 8 && kinds >= 2 { return "fair" } "weak"}component PasswordInput(name: str = "password", label: str = "Password", value: str = "", placeholder: str? = null, hint: str? = null, error: str? = null, required: bool = false, disabled: bool = false, field: str? = null, size: str = "md", new: bool = false, meter: bool = false, autocomplete: str? = null, on_input = null, class: str? = null) { style global password_input_css state text = value state shown = false let fid = field ?? `password-input-${name}` let strength = password_input_strength(text) let described = ui_cls([hint != null && `${fid}-hint`, meter && `${fid}-meter`, error != null && `${fid}-error`]) <div class={ui_cls(["ui-password-input", size == "sm" && "ui-password-input-sm", class])}> <label.ui-password-input-label for={fid}> {label}{required && <span.ui-password-input-req aria-hidden="true">*</span>} </label> <div.ui-password-input-box> <input.ui-password-input-control type={shown ? "text" : "password"} id={fid} name={name} value={text} placeholder={placeholder} required={required} disabled={disabled} autocomplete={autocomplete ?? (new ? "new-password" : "current-password")} autocapitalize="off" spellcheck="false" aria-invalid={error != null ? "true" : null} aria-describedby={described != "" ? described : null} @input={(e) => { text = e.value on_input && on_input(text) }}/> <button.ui-password-input-toggle type="button" aria-controls={fid} 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> {meter && <p class={ui_cls(["ui-password-input-meter", strength != "" && `ui-password-input-${strength}`])} id={`${fid}-meter`} aria-live="polite"> <span.ui-password-input-bars aria-hidden="true"><span></span><span></span><span></span></span> <span.ui-password-input-word>{strength != "" ? `Strength: ${strength}` : "Strength: none yet"}</span> </p>} {hint != null && <p.ui-password-input-hint id={`${fid}-hint`}>{hint}</p>} {error != null && <p.ui-password-input-error id={`${fid}-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 downloadsOtpInputBoxes for a one-time code: typing moves along, Backspace goes back, pasting fills them all.5 views · 5 downloadsPasswordStrengthA new-password field with a meter that fills as it gets stronger and a checklist of rules ticked as they are met.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 downloadsAutocompleteA free-text box that offers matches as one types, with the arrow keys and Enter; whatever is typed may stay.5 views · 7 downloads
Thread
Thread 0 posts
On PasswordInput: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.