Forms

PasswordInput

A password field with a show/hide toggle and, for new passwords, a strength meter.

#forms#input#security#interactive

Demo

Preview

Strength: none yet

At least 12 characters, with upper and lower case and a digit.

0 characters typed.

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

Thread 0 posts

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

No posts yet. The first one opens the thread.