Password input — Usage
Try it
Section titled “Try it”Type a password, then click the eye. Put your caret in the middle of the text first — it stays
where you left it. (PasswordInput, commonRules and useState are already in scope.)
function TryIt() { const [value, setValue] = useState('') const [valid, setValid] = useState(false) return ( <div style={{ display: 'grid', gap: '0.75rem', maxWidth: '22rem' }}> <PasswordInput label="Choose a password" autoComplete="new-password" showStrength minScore={2} minLength={10} userInputs={['ada@example.com', 'Ada Lovelace']} rules={[commonRules.digit, { ...commonRules.symbol, optional: true }]} value={value} onChange={setValue} onValidityChange={setValid} /> <small>{valid ? 'Ready to submit' : 'Not yet'}</small> </div> ) }
Try password, P4ssw0rd, qwertyuiop, ada@example.com and
correct-horse-battery-staple — the first four are all penalised for different reasons.
Sign in
Section titled “Sign in”The minimum useful field. Masked, with a reveal toggle and a Caps Lock warning.
import { PasswordInput } from '@rxova/react-password-input'
function SignIn() { return <PasswordInput label="Password" name="password" autoComplete="current-password" />}autoComplete defaults to current-password. Set it to new-password on sign-up and
change-password forms — with the wrong value, password managers offer the old password instead of
generating a new one. onWarn catches an empty or off value, but it cannot detect this mix-up,
because only your app knows which form it is.
The full surface is PasswordInputProps in the published types; the
API reference is generated from them. The ones you reach for first:
| Prop | Type | Default | Notes |
|---|---|---|---|
value / defaultValue / onChange | string | — | onChange emits the string, not an event |
label | ReactNode | — | Accessible name. Not rendered — bring your own visible <label> |
autoComplete | string | 'current-password' | Set 'new-password' to sign up or change a password |
showStrength | boolean | false | Renders the meter |
minScore | PasswordScore | null | null | Minimum 0–4 score for validity |
rules / showRules | PasswordRule[] / boolean | one length rule | NIST leads with length, so the default is length alone |
minLength / maxLength | number | 8 / 128 | The cap is always applied; 128 is double NIST’s 64-char floor |
onValidityChange | (valid: boolean) => void | — | Fires when “all rules met and minScore reached” flips |
checkCompromised | (password, signal) => Promise<boolean> | — | Your breach lookup; the library never makes a request |
hideRevealToggle / hideOnBlur | boolean | false / true | The reveal button, and whether leaving re-masks |
capsLockWarning | boolean | true | Read off the real modifier state |
autoFocus / aria-label | boolean / string | — | aria-label wins over label |
invalid / disabled / readOnly / required | boolean | false | Mirrored onto aria-* and data-* |
onWarn | (warning: PasswordWarning) => void | console.warn | Development only; stripped from production builds |
Controlled and uncontrolled
Section titled “Controlled and uncontrolled”Both the value and the reveal state can be controlled independently.
import { useState } from 'react'import { PasswordInput } from '@rxova/react-password-input'
function Controlled() { const [value, setValue] = useState('') const [revealed, setRevealed] = useState(false)
return ( <PasswordInput label="Password" value={value} onChange={setValue} revealed={revealed} onRevealChange={setRevealed} /> )}onRevealChange fires only on an actual change, so it is safe to wire straight into state.
Requirements
Section titled “Requirements”The default rule set is one rule, about length. See About for why. Pass rules for
anything more.
import { PasswordInput, commonRules } from '@rxova/react-password-input'
function Field() { return ( <PasswordInput label="Password" rules={[ { id: 'length', label: 'At least 12 characters', test: (p) => Array.from(p).length >= 12 }, commonRules.digit, { ...commonRules.symbol, optional: true }, ]} /> )}A rule is { id, label, test, optional? }. An optional rule shows in the checklist but never
blocks validity. A test that throws is reported as unmet rather than taking the field down.
Confirm password
Section titled “Confirm password”A second field that has to match the first. The match is the parent’s business, not the component’s
— invalid and aria-describedby are the two props that wire it up, and the error text is yours.
Two details this example gets right and hand-rolled versions usually do not: the mismatch is only
announced once the confirm field has been touched, so it does not shout at someone who has not
typed there yet, and autoComplete="new-password" is set on both so a password manager offers to
generate rather than autofill.
function SignUp() { const [password, setPassword] = useState('') const [confirm, setConfirm] = useState('') const [touched, setTouched] = useState(false) const mismatch = touched && confirm.length > 0 && confirm !== password return ( <div style={{ display: 'grid', gap: '1rem', maxWidth: '22rem' }}> <PasswordInput label="Password" autoComplete="new-password" showStrength value={password} onChange={setPassword} /> <div> <PasswordInput label="Confirm password" autoComplete="new-password" value={confirm} onChange={setConfirm} onBlur={() => { setTouched(true) }} invalid={mismatch} aria-describedby={mismatch ? 'confirm-error' : undefined} /> {mismatch ? ( <p id="confirm-error" style={{ color: 'crimson', margin: '0.25rem 0 0' }}> Both passwords must match. </p> ) : null} </div> <p> {password.length === 0 ? 'Enter a password.' : confirm === password ? 'Passwords match.' : 'Passwords do not match yet.'} </p> </div> ) }
The error element is referenced by aria-describedby only while it exists, so a screen reader
reads the message when there is one and nothing when there is not. The component merges that id with
the ones it owns for the strength meter and checklist rather than overwriting them.
With a form library
Section titled “With a form library”The same shape works with a resolver — the confirm field’s invalid comes from the library’s error
state instead of local state:
const schema = z .object({ password: z.string().min(12), confirm: z.string(), }) .refine((values) => values.password === values.confirm, { message: 'Both passwords must match.', path: ['confirm'], })
<Controller name="confirm" control={control} render={({ field, fieldState }) => ( <PasswordInput label="Confirm password" autoComplete="new-password" value={field.value} onChange={field.onChange} onBlur={field.onBlur} invalid={fieldState.invalid} /> )}/>The rendered element is an ordinary <input>, so every form library works without an adapter and
the ref lands on the input itself.
import { useForm } from 'react-hook-form'import { PasswordInput } from '@rxova/react-password-input'
function SignIn() { const { register, handleSubmit } = useForm<{ password: string }>() const { ref, onChange, ...field } = register('password', { required: true })
return ( <form onSubmit={handleSubmit(() => undefined)}> <PasswordInput label="Password" ref={ref} onChange={(value) => onChange({ target: { name: field.name, value } })} {...field} /> <button type="submit">Sign in</button> </form> )}Styling
Section titled “Styling”No stylesheet to import. Only layout-critical declarations are inlined; everything visual is a CSS
custom property or a data-* hook.
[data-rx-password-root] { --rx-password-meter-height: 0.375rem; --rx-password-meter-fill-1: #d64545; --rx-password-meter-fill-2: #e08c2e; --rx-password-meter-fill-3: #b5c334; --rx-password-meter-fill-4: #3fa34d;}
[data-rx-password-rules] li[data-met] { color: #3fa34d;}The full list of properties and data-* hooks is in the
package README.
The data-* attributes are public API, covered by semver.
Do not shrink --rx-password-toggle-size below 1.5rem: at the default font size that is the 24×24 CSS
pixels WCAG 2.5.8 requires.
Headless
Section titled “Headless”usePasswordInput gives you the whole state machine with no markup — including the two parts that
are genuinely hard: caret restoration across the type swap, and the abortable, debounced breach
check.
import { usePasswordInput } from '@rxova/react-password-input'
function CustomField() { const field = usePasswordInput({ minLength: 10 })
return ( <div> <input ref={(node) => { field.inputRef.current = node }} id={field.ids.input} type={field.type} value={field.value} onChange={(event) => { field.setValue(event.target.value) }} onKeyDown={field.handleModifierEvent} onKeyUp={field.handleModifierEvent} onBlur={field.handleBlur} /> <button type="button" aria-pressed={field.revealed} onMouseDown={(event) => { field.captureSelection() event.preventDefault() }} onClick={field.toggleReveal} > {field.revealed ? 'Hide' : 'Show'} </button> {field.capsLock ? <p role="status">Caps Lock is on</p> : null} </div> )}captureSelection on mousedown is not optional if you want the caret preserved — see
About.