This is the password-rules convention as components: rules stated as positive assertions, live feedback, an eye toggle on the field, and the rules list doubling as the error state.
Import
import { PasswordInput, PasswordRulesList } from "@timelycare/helix-ui"
Props
interface PasswordInputProps extends Omit<React.ComponentProps<"input">, "type"> {
showPasswordLabel?: string // default "Show password"
hidePasswordLabel?: string // default "Hide password"
hideToggle?: boolean // the convention is to keep the toggle
}
interface PasswordRule {
id: string
label: React.ReactNode // positive assertion, already translated
test: (value: string) => boolean
}
interface PasswordRulesListProps {
value: string
rules: PasswordRule[]
title?: React.ReactNode
showStrength?: boolean // default false
strengthLabels?: [string, string, string, string]
strengthLabel?: string
className?: string
}
Usage
Sign-in
The toggle is the whole point here; no rules list on a field the user is recalling rather than choosing.
<Label htmlFor="password">Password</Label>
<PasswordInput id="password" autoComplete="current-password" />
Set or change a password
const RULES = [
{ id: "length", label: "Must have 10 characters", test: (v) => v.length >= 10 },
{ id: "upper", label: "Must contain 1 capital letter", test: (v) => /[A-Z]/.test(v) },
{ id: "number", label: "Must contain 1 number", test: (v) => /\d/.test(v) },
]
<Label htmlFor="new-password">New password</Label>
<PasswordInput
id="new-password"
autoComplete="new-password"
value={value}
onChange={(e) => setValue(e.target.value)}
/>
<PasswordRulesList value={value} rules={RULES} />
Submit stays disabled until every rule passes — see the wizard footer's disabled-until-valid rule.
The rules come from the product
rules is caller-supplied on purpose. Password policy is owned by each product and enforced by its server; the component renders whatever policy it is handed.
A list that tells someone their password is acceptable while the API rejects it is worse than showing no list at all, so the list must mirror what is actually enforced.
Structure
| Element | Tailwind |
|---|---|
| Field wrapper | relative |
| Input | Input + pr-10 to clear the toggle |
| Toggle | absolute inset-y-0 right-0 px-3, size-4 icon |
| Rule row | flex items-center gap-2 text-sm |
| Met rule | text-foreground + CheckIcon text-success |
| Unmet rule | text-muted-foreground + XIcon |
| Strength track / fill | bg-muted / bg-primary |
Strength bar
Off by default. When enabled with showStrength, it is a summary of how many rules pass, rendered as a progressbar.
It is deliberately not an entropy estimate, and it is secondary to the list: the rules tell someone what to do, a bar only tells them how they are doing.
<PasswordRulesList value={value} rules={RULES} showStrength />
Accessibility
- The toggle is a real
<button type="button">with anaria-labelthat swaps between show and hide, andaria-pressedreflecting state. - Toggling visibility does not move focus or reset the caret.
- Each rule row carries visually hidden "met" or "not met" text, so state is never conveyed by icon or color alone.
- The strength bar is a
progressbarwitharia-valuetextnaming the level, not just a number. - Pass every string already translated; the library does no i18n and the defaults are English.
- Do not disable paste. It breaks password managers, which makes people choose worse passwords.
Gotchas
| Problem | Solution |
|---|---|
| Rules pass but the server rejects the password | The list does not mirror the enforced policy; fix the rules array |
| Rules read like errors | State them as positive assertions ("Must have 10 characters"), not failures |
| A separate error message under the list | The list is the error state; remove the duplicate |
| Icon overlaps long values | Keep the pr-10 on the input if you pass a custom className |
| Browser offers the wrong autofill | Set autoComplete="current-password" for sign-in, "new-password" for set/change |