pwd-validator-react
v0.1.0
Published
Modular, accessible React password validator with strength meter
Maintainers
Readme
pwd-validator-react
Modular, accessible React password validator with strength meter, customisable rules, and optional styling.
Features
- Modular — each component is usable on its own, or use
<PasswordField>as an all-in-one solution - Headless Core —
validatePassword()andgetStrength()work without React - HIBP —
checkPwned()as a separate entry point (pwd-validator-react/hibp) - i18n — 12 built-in locales (
en,de,fr,it,nl,es,tr,he,ar,pt,pl,ja) with RTL support and alocaleprop on every component - TypeScript — fully typed with exported types
- Accessibility — ARIA attributes, keyboard navigation, screen reader support
- Mobile First — responsive from 280 px, 16 px minimum font on mobile (prevents iOS zoom)
- React 18 & 19 — compatible with React 18+ and React 19+
- Next.js compatible —
"use client"directive on all components and hooks - CSS variables — all colours, spacing, and radii configurable via custom properties
- Debounce — optional validation delay while typing
- Callbacks —
onStrengthChange,onValidChange,onMatchChange - Password confirmation — optional second field with match checking
- Have I Been Pwned — optional check against known data breaches via k-Anonymity
- Overlay mode — strength bar and messages in an optional floating popup (opens above or below the input automatically)
- Password generator — cryptographically secure
generatePassword()function and optional UI button - Overlay callbacks —
onOverlayOpen,onOverlayClose, andpopupWidthfor the overlay - matchesPassword — rule factory for password-confirmation validation
- fireOnMount — optionally fire
onStrengthChange/onValidChangeimmediately on mount - Zod integration —
toZod(rules)as a separate entry point (pwd-validator-react/zod) for schema validation
Table of Contents
- Installation
- Quick Start
- React 19 Compatibility
- Internationalisation (i18n)
- Architecture
- PasswordField (All-in-One)
- PasswordInput
- StrengthBar
- ValidationMessages
- usePasswordValidation (Hook)
- Headless Core (without React)
- Validation Rules
- Strength Calculation
- Debounce
- Callbacks
- matchesPassword
- fireOnMount
- Zod Integration
- Password Confirmation
- Overlay Mode
- Password Generator
- Have I Been Pwned (HIBP)
- Styling & Theming
- Accessibility
- Next.js
- Controlled vs. Uncontrolled
- TypeScript Types
- Development
- License
Installation
npm install pwd-validator-reactReact 18+ or 19+ is required as a peer dependency.
Quick Start
import { PasswordField } from 'pwd-validator-react';
import 'pwd-validator-react/styles.css';
function Registration() {
return (
<PasswordField showToggle label="Password" />
);
}That's it. PasswordField includes the input, strength bar, and validation messages. With no additional props the default English rules are used (8 characters, upper/lowercase, number, special character).
To use a different locale, pass the locale prop:
import { PasswordField } from 'pwd-validator-react';
import { de } from 'pwd-validator-react/locales';
import 'pwd-validator-react/styles.css';
function Registration() {
return <PasswordField showToggle locale={de} />;
}React 19 Compatibility
pwd-validator-react is fully compatible with React 19. The test suite (263 tests) runs against React 19.2.4. No workarounds are needed — the package works identically under React 18 and React 19.
The peer dependency is react >= 18.0.0, so both major versions are supported.
Internationalisation (i18n)
The package supports multiple languages via locale objects. English is used by default.
Built-in Locales
en (English) is included in the main bundle. All other locales are imported from pwd-validator-react/locales:
import { en } from 'pwd-validator-react';
import { de, fr, it, nl, es, tr, he, ar, pt, pl, ja } from 'pwd-validator-react/locales';
import type { PvrLocale } from 'pwd-validator-react';| Locale | Language | Direction |
|--------|----------|-----------|
| en | English | LTR |
| de | German | LTR |
| fr | French | LTR |
| it | Italian | LTR |
| nl | Dutch | LTR |
| es | Spanish | LTR |
| tr | Turkish | LTR |
| he | Hebrew | RTL |
| ar | Arabic | RTL |
| pt | Portuguese | LTR |
| pl | Polish | LTR |
| ja | Japanese | LTR |
Tree-shaking:
enis in the main entry point (pwd-validator-react). All other locales live inpwd-validator-react/locales. Unused locales are automatically removed by your bundler.
locale Prop
All components (PasswordField, PasswordInput, StrengthBar) and the hook (usePasswordValidation) accept a locale prop:
import { PasswordField } from 'pwd-validator-react';
import { de } from 'pwd-validator-react/locales';
// German UI:
<PasswordField locale={de} showToggle />
// English UI (default — locale prop optional):
<PasswordField showToggle />String Resolution Priority
- Explicit prop (e.g.
label="Enter password") — highest priority - Locale object (e.g.
locale={de}provideslabel="Passwort") - English default (e.g.
label="Password")
// locale=de provides "Passwort" as the label:
<PasswordField locale={de} />
// Explicit label overrides the locale:
<PasswordField locale={de} label="Secret" />createDefaultRules
createDefaultRules(locale) returns the 5 default validation rules with error messages from the given locale:
import { createDefaultRules } from 'pwd-validator-react';
import { de } from 'pwd-validator-react/locales';
// German messages:
const rules = createDefaultRules(de);
// [
// { name: 'minLength', message: 'Mindestens 8 Zeichen', ... },
// { name: 'hasUppercase', message: 'Mindestens 1 Großbuchstabe', ... },
// ...
// ]
// English messages (default):
const rules = createDefaultRules();
// [
// { name: 'minLength', message: 'At least 8 characters', ... },
// ...
// ]Note: The
defaultRulesconstant always uses English messages. UsecreateDefaultRules(locale)for localised rules.
Creating a Custom Locale
Use defineLocale to create a custom locale. Any omitted field falls back to the English default:
import { defineLocale } from 'pwd-validator-react';
const customLocale = defineLocale({
label: 'Passwort',
// only override the fields you need — the rest fall back to English
});
<PasswordField locale={customLocale} showToggle />For a complete locale, provide all fields:
import type { PvrLocale } from 'pwd-validator-react';
const frLocale: PvrLocale = {
minLength: (min) => `Au moins ${min} caractères`,
maxLength: (max) => `Au plus ${max} caractères`,
hasUppercase: (min) => `Au moins ${min} majuscule${min === 1 ? '' : 's'}`,
hasLowercase: (min) => `Au moins ${min} minuscule${min === 1 ? '' : 's'}`,
hasNumber: (min) => `Au moins ${min} chiffre${min === 1 ? '' : 's'}`,
hasSpecialChar: (min) => `Au moins ${min} caractère${min === 1 ? '' : 'aux'} spécial${min === 1 ? '' : 'aux'}`,
label: 'Mot de passe',
showPassword: 'Afficher le mot de passe',
hidePassword: 'Masquer le mot de passe',
strengthLabel: 'Force du mot de passe',
strengthLevels: ['Faible', 'Moyen', 'Fort', 'Très fort'],
confirmLabel: 'Confirmer le mot de passe',
confirmError: 'Les mots de passe ne correspondent pas',
pwnedMessage: 'Ce mot de passe a été trouvé dans une fuite de données',
pwnedChecking: 'Vérification...',
generatePassword: 'Générer un mot de passe',
};
<PasswordField locale={frLocale} showToggle showConfirm />RTL Support
The he (Hebrew) and ar (Arabic) locales automatically set dir="rtl" on all components. No additional configuration is needed.
import { ar } from 'pwd-validator-react/locales';
// Arabic UI with automatic RTL layout:
<PasswordField locale={ar} showToggle showConfirm />Custom RTL locales can set dir: 'rtl':
import { defineLocale } from 'pwd-validator-react';
const myRtlLocale = defineLocale({
dir: 'rtl',
label: 'סיסמה',
// ...
});maxLength Rule
In addition to minLength, an optional maxLength rule is available:
import { maxLength, createDefaultRules } from 'pwd-validator-react';
// English message:
const rules = [...createDefaultRules(), maxLength(50)];
// With locale:
import { de } from 'pwd-validator-react/locales';
const rules = [...createDefaultRules(de), maxLength(50, de)];Architecture
The package is built in three layers — each layer can be used independently:
Layer 3: Components <PasswordField> <PasswordInput> <StrengthBar> <ValidationMessages>
Layer 2: Hook usePasswordValidation()
Layer 1: Core validatePassword() getStrength() defaultRules createDefaultRules()
checkPwned() via 'pwd-validator-react/hibp'
de/fr/it/nl/es/tr/he/ar via 'pwd-validator-react/locales'Layer 1 (Core) — pure functions with no React dependency. Can be used server-side, in tests, or in non-React projects. checkPwned() is bundled in its own entry point (pwd-validator-react/hibp).
Layer 2 (Hook) — React hook with state management, debounce, and HIBP integration. Use this for custom UI implementations.
Layer 3 (Components) — ready-made UI components. PasswordField combines everything; the individual components can be composed freely.
PasswordField (All-in-One)
Combines PasswordInput, StrengthBar, and ValidationMessages in one component. Supports all features (debounce, callbacks, confirmation, HIBP, i18n) via props.
Basic Usage
import { PasswordField } from 'pwd-validator-react';
import 'pwd-validator-react/styles.css';
// Minimal (English):
<PasswordField />
// With locale:
import { de } from 'pwd-validator-react/locales';
<PasswordField locale={de} />
// With visibility toggle and classic variant:
<PasswordField variant="classic" showToggle label="New Password" />
// Without strength bar or messages:
<PasswordField showStrengthBar={false} showMessages={false} />All Features Combined
import { PasswordField } from 'pwd-validator-react';
import { de } from 'pwd-validator-react/locales';
<PasswordField
variant="floating"
locale={de}
showToggle
rules={myRules}
strengthConfig={{ minStrength: 3 }}
debounceMs={300}
showConfirm
checkPwned
onStrengthChange={(s) => console.log('Strength:', s)}
onValidChange={(v) => console.log('Valid:', v)}
onMatchChange={(m) => console.log('Match:', m)}
/>Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| locale | PvrLocale | en | Locale object for all UI strings. Automatically sets dir when locale.dir is defined (e.g. 'rtl' for he/ar) |
| variant | 'floating' \| 'classic' | 'floating' | Input variant: floating label (MUI-style) or classic (label above) |
| label | string | locale.label | Label text |
| showToggle | boolean | true | Show the show/hide password button |
| showStrengthBar | boolean | true | Show the strength bar below the input |
| showMessages | boolean | true | Show validation messages |
| showValidRules | boolean | true | Show passing rules with a checkmark (only when showMessages is active) |
| rules | ValidationRule[] | createDefaultRules(locale) | Validation rules |
| strengthLabels | [string, string, string, string] | locale.strengthLevels | Labels for the 4 strength levels |
| strengthConfig | StrengthConfig | — | Customise strength calculation |
| debounceMs | number | 0 | Validation delay in milliseconds |
| onStrengthChange | (strength: PasswordStrength) => void | — | Fires when strength changes |
| onValidChange | (isValid: boolean) => void | — | Fires when validity changes |
| fireOnMount | boolean | false | Fire callbacks immediately on mount, even without user input |
| showConfirm | boolean | false | Show confirmation field |
| confirmLabel | string | locale.confirmLabel | Confirmation field label |
| confirmError | string | locale.confirmError | Error message when passwords do not match |
| onMatchChange | (matches: boolean) => void | — | Fires when the match status changes |
| checkPwned | boolean | false | Enable HIBP check |
| pwnedMessage | string | locale.pwnedMessage | Warning shown when password is found in a breach |
| autoComplete | string | 'off' | Controls browser autocomplete |
| value | string | — | Controlled mode: current value |
| onChange | (e: ChangeEvent) => void | — | Change handler (provides e.target.value) |
| overlayMode | boolean | false | Show strength bar and messages in a floating overlay instead of inline |
| onOverlayOpen | () => void | — | Fires when the overlay opens |
| onOverlayClose | () => void | — | Fires when the overlay closes (on blur) |
| popupWidth | string \| number | '100%' | Width of the overlay popup. Numbers are treated as pixels (e.g. 300 → 300px) |
| showStrengthLabel | boolean | true (inline) / false (overlay) | Show the text label below the strength bar |
| minStrength | 0 \| 1 \| 2 \| 3 \| 4 | 2 | Shorthand for strengthConfig.minStrength. Takes precedence over strengthConfig when both are set |
| showGenerator | boolean | false | Show the password generator button |
| className | string | — | Additional CSS class |
| id | string | — | HTML ID for the input element |
| placeholder | string | — | Placeholder text |
All native <input> attributes are also supported (except type, which is always password or text).
PasswordInput
The raw input field without strength bar or validation messages. Ideal when you want to control validation yourself.
import { PasswordInput } from 'pwd-validator-react';
import 'pwd-validator-react/styles.css';
// Floating variant (default):
<PasswordInput
label="Password"
showToggle
value={password}
onChange={(e) => setPassword(e.target.value)}
error={touched && !isValid}
/>
// Classic variant:
<PasswordInput variant="classic" label="Current Password" showToggle />
// With locale:
import { de } from 'pwd-validator-react/locales';
<PasswordInput locale={de} showToggle />Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| locale | PvrLocale | en | Locale for label and toggle aria-labels. Sets dir automatically for RTL locales |
| variant | 'floating' \| 'classic' | 'floating' | Input variant |
| label | string | locale.label | Label text |
| showToggle | boolean | false | Show the visibility toggle |
| error | boolean | false | Visual error state (red border) |
Supports ref forwarding (forwardRef) and all native <input> attributes.
Variants
Floating (default): The label floats above the input on focus or when a value is present — similar to Material UI. The placeholder is hidden; the label serves as the placeholder.
Classic: The label sits above the input at all times. The placeholder is visible.
StrengthBar
Visualises password strength as a 4-segment bar with an optional label.
import { StrengthBar } from 'pwd-validator-react';
// Option A: pass strength value directly (0–4):
<StrengthBar strength={3} />
// Option B: pass password, strength is calculated automatically:
<StrengthBar password="MyPassword123!" />
// With custom rules:
<StrengthBar password={value} rules={myRules} />
// With custom labels:
<StrengthBar strength={2} labels={['Weak', 'Fair', 'Good', 'Strong']} />
// With locale:
import { de } from 'pwd-validator-react/locales';
<StrengthBar strength={2} locale={de} />
// With custom StrengthConfig:
<StrengthBar password={value} strengthConfig={{ entropyThresholds: [30, 50, 70] }} />Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| locale | PvrLocale | en | Locale for labels and aria-label. Sets dir automatically for RTL locales |
| strength | 0 \| 1 \| 2 \| 3 \| 4 | — | Strength value passed directly |
| password | string | — | Password for automatic calculation (alternative to strength) |
| rules | ValidationRule[] | defaultRules | Rules used for automatic calculation |
| strengthConfig | StrengthConfig | — | Customise the strength calculation |
| labels | [string, string, string, string] | locale.strengthLevels | Labels for the 4 levels |
| showStrengthLabel | boolean | true | Show the text label below the bar. false adds the CSS class pvr-strength-bar--no-label |
| className | string | — | Additional CSS class |
When neither strength nor password is provided, the bar shows strength 0 (empty).
Strength Levels
| Level | Colour | CSS Variable | Meaning |
|-------|--------|-------------|---------|
| 0 | Grey | --pvr-strength-bg | No password |
| 1 | Red | --pvr-strength-weak | Weak |
| 2 | Orange | --pvr-strength-fair | Fair |
| 3 | Green | --pvr-strength-good | Strong |
| 4 | Dark green | --pvr-strength-strong | Very strong |
ValidationMessages
Displays validation messages as a list with a checkmark (passing) or cross (failing) for each rule.
import { ValidationMessages } from 'pwd-validator-react';
// Option A: pass errors directly:
<ValidationMessages errors={[{ rule: 'minLength', message: 'At least 8 characters' }]} />
// Option B: pass password, errors are calculated automatically:
<ValidationMessages password="abc" />
// Show passing rules with a checkmark:
<ValidationMessages password="MyPassword123!" showValid />
// With custom rules:
<ValidationMessages password={value} rules={myRules} showValid />Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| errors | ValidationError[] | — | Errors passed directly |
| password | string | — | Password for automatic calculation (alternative to errors) |
| rules | ValidationRule[] | defaultRules | Rules for automatic calculation |
| showValid | boolean | false | Show passing rules with a checkmark |
| strengthConfig | StrengthConfig | — | Config (for isValid threshold) |
| className | string | — | Additional CSS class |
| id | string | — | HTML ID for aria-describedby linkage |
| role | 'alert' \| 'status' | 'alert' | ARIA role: alert (assertive) for errors, status (polite) for live updates |
usePasswordValidation (Hook)
React hook with full state management. Ideal for custom UI implementations or when composing the individual components yourself.
Basic Usage
import { usePasswordValidation, PasswordInput, StrengthBar, ValidationMessages } from 'pwd-validator-react';
import 'pwd-validator-react/styles.css';
function MyForm() {
const { password, setPassword, isValid, errors, strength, touched } = usePasswordValidation();
return (
<form onSubmit={(e) => { e.preventDefault(); if (isValid) alert('OK!'); }}>
<PasswordInput
showToggle
value={password}
onChange={(e) => setPassword(e.target.value)}
error={touched && !isValid}
/>
<StrengthBar strength={strength} />
<ValidationMessages
errors={touched ? errors : []}
password={touched ? password : undefined}
showValid={touched}
/>
<button type="submit" disabled={!isValid}>Submit</button>
</form>
);
}All Options
import { de } from 'pwd-validator-react/locales';
const validation = usePasswordValidation({
locale: de, // locale for rule messages (default: en)
rules: myRules, // custom rules (default: createDefaultRules(locale))
debounceMs: 300, // delay validation while typing
strengthConfig: { minStrength: 3 }, // strength configuration
onStrengthChange: (s) => {}, // callback when strength changes
onValidChange: (v) => {}, // callback when validity changes
showConfirm: true, // enable confirmation field
checkPwned: true, // enable HIBP check
fireOnMount: true, // fire callbacks immediately on mount
});Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| locale | PvrLocale | en | Locale for rule messages |
| rules | ValidationRule[] | createDefaultRules(locale) | Validation rules |
| debounceMs | number | 0 | Delay in milliseconds |
| strengthConfig | StrengthConfig | — | Customise strength calculation |
| onStrengthChange | (strength: PasswordStrength) => void | — | Fires when strength changes |
| onValidChange | (isValid: boolean) => void | — | Fires when validity changes |
| showConfirm | boolean | false | Enable confirmation field |
| checkPwned | boolean | false | Enable HIBP check |
| fireOnMount | boolean | false | Fire callbacks once on mount |
Return Values
| Value | Type | Description |
|-------|------|-------------|
| password | string | Current password value |
| setPassword | (value: string) => void | Set the password (also sets touched to true) |
| isValid | boolean | true when strength >= minStrength (default: 2 = Fair) |
| errors | { rule: string; message: string }[] | List of failing rules |
| strength | 0 \| 1 \| 2 \| 3 \| 4 | Calculated password strength |
| touched | boolean | true once the user has typed something |
| confirmPassword | string | Current confirmation value |
| setConfirmPassword | (value: string) => void | Set the confirmation value |
| passwordsMatch | boolean | true when password and confirmation match (or showConfirm is off) |
| isPwned | boolean \| null | true = found in breach, false = not found, null = not checked or error |
| pwnedCount | number | How many times the password appeared in breaches |
| isPwnedLoading | boolean | true while the HIBP request is in flight |
Example with Confirmation and HIBP
import { de } from 'pwd-validator-react/locales';
function RegistrationForm() {
const {
password, setPassword,
confirmPassword, setConfirmPassword,
isValid, passwordsMatch,
isPwned, isPwnedLoading,
strength, errors, touched,
} = usePasswordValidation({
locale: de,
showConfirm: true,
checkPwned: true,
debounceMs: 300,
});
const canSubmit = isValid && passwordsMatch && !isPwned && !isPwnedLoading;
return (
<form>
<input type="password" value={password} onChange={(e) => setPassword(e.target.value)} />
<input type="password" value={confirmPassword} onChange={(e) => setConfirmPassword(e.target.value)} />
{touched && !passwordsMatch && <p>Passwords do not match</p>}
{isPwned && <p>This password was found in a data breach!</p>}
{isPwnedLoading && <p>Checking password...</p>}
<button type="submit" disabled={!canSubmit}>Register</button>
</form>
);
}Headless Core (without React)
The core functions work without React and can be used server-side, in tests, or in other frameworks.
validatePassword
Checks a password against rules and calculates its strength.
import { validatePassword } from 'pwd-validator-react';
const result = validatePassword('MyP@ssw0rd!');
// {
// isValid: true,
// errors: [],
// strength: 3
// }
const result2 = validatePassword('abc');
// {
// isValid: false,
// errors: [
// { rule: 'minLength', message: 'At least 8 characters' },
// { rule: 'hasUppercase', message: 'At least 1 uppercase letter' },
// { rule: 'hasNumber', message: 'At least 1 number' },
// { rule: 'hasSpecialChar', message: 'At least 1 special character' },
// ],
// strength: 1
// }
// With custom rules:
const result3 = validatePassword('test', myRules);
// With StrengthConfig:
const result4 = validatePassword('test', defaultRules, { minStrength: 3 });
// With a localised rule set:
import { createDefaultRules } from 'pwd-validator-react';
import { de } from 'pwd-validator-react/locales';
const result5 = validatePassword('abc', createDefaultRules(de));
// result5.errors[0].message === 'Mindestens 8 Zeichen'Note:
isValidis based on strength, not individual rules. A password can haveisValid: trueand still haveerrors(e.g. at strength 2 it may lack special characters but the entropy is sufficient). By default a password is valid at strength 2 (Fair) or above.
getStrength
Calculates only the strength without a full validation run.
import { getStrength } from 'pwd-validator-react';
getStrength(''); // 0 (empty)
getStrength('abc'); // 1 (weak)
getStrength('Abcdefgh'); // 2 (fair)
getStrength('Abcdefg1'); // 3 (strong)
getStrength('Abcdefghij1!'); // 4 (very strong)
// With custom rules or config:
getStrength('test', myRules, { entropyThresholds: [20, 35, 50] });checkPwned
Checks a password against the HaveIBeenPwned database.
Important:
checkPwnedmust be imported from the separate entry pointpwd-validator-react/hibp, not from the main bundle.
import { checkPwned } from 'pwd-validator-react/hibp';
const { isPwned, count } = await checkPwned('password123');
// isPwned: true
// count: 12345 (how many times found in breaches)
// With AbortSignal for cancellation:
const controller = new AbortController();
const result = await checkPwned('test', controller.signal);
controller.abort(); // cancels the requestThe function uses k-Anonymity: only the first 5 characters of the SHA-1 hash are sent to the API. The full password never leaves the client. The Add-Padding: true header additionally protects against traffic analysis.
Bundle splitting:
checkPwnedlives in a separate entry point (pwd-validator-react/hibp) so that SHA-1 hashing logic is not included in the main bundle unless needed. Components and the hook usecheckPwnedvia dynamic imports, so the bundle is only loaded on demand.
generatePassword
Generates a cryptographically secure password.
import { generatePassword, createDefaultRules } from 'pwd-validator-react';
// Simple:
const pw = generatePassword(); // 16 characters, all character classes
// Matched to active rules (recommended):
const pw = generatePassword({ rules: createDefaultRules() });
// -> minimum length from minLength rule, active character classes derived from rule names
// With options:
const pw = generatePassword({
length: 20,
charset: {
uppercase: true,
lowercase: true,
numbers: true,
special: '!@#$%', // custom special characters, or false to disable
},
});Uses crypto.getRandomValues for cryptographic randomness. Guarantees all required character classes are present in the generated password.
Validation Rules
Default Rules
The package ships 5 predefined rules (defaultRules). Messages default to English:
| Rule | Description | Message (en) | Message (de) |
|------|-------------|-------------|-------------|
| minLength(8) | At least 8 characters | "At least 8 characters" | "Mindestens 8 Zeichen" |
| hasUppercase(min?) | At least min uppercase letters (default: 1) | "At least 1 uppercase letter" | "Mindestens 1 Großbuchstabe" |
| hasLowercase(min?) | At least min lowercase letters (default: 1) | "At least 1 lowercase letter" | "Mindestens 1 Kleinbuchstabe" |
| hasNumber(min?) | At least min digits (default: 1) | "At least 1 number" | "Mindestens 1 Zahl" |
| hasSpecialChar(min?) | At least min special characters (default: 1) | "At least 1 special character" | "Mindestens 1 Sonderzeichen" |
The optional maxLength(n) rule is not included in defaultRules:
| Rule | Description | Message (en) |
|------|-------------|-------------|
| maxLength(100) | At most 100 characters | "At most 100 characters" |
import { maxLength, createDefaultRules } from 'pwd-validator-react';
import { de } from 'pwd-validator-react/locales';
const rules = [...createDefaultRules(de), maxLength(50, de)];Adjusting Default Rules
import { minLength, hasUppercase, hasLowercase, hasNumber, hasSpecialChar } from 'pwd-validator-react';
import { de } from 'pwd-validator-react/locales';
// Standard (min=1 for all):
const rules = createDefaultRules(de);
// Custom — at least 2 uppercase letters and 2 digits:
const rules = [
minLength(8, de),
hasUppercase(2, de), // "Mindestens 2 Großbuchstaben"
hasLowercase(1, de),
hasNumber(2, de), // "Mindestens 2 Zahlen"
hasSpecialChar(1, de),
];Creating Rules with createRule
import { createRule } from 'pwd-validator-react';
const noSpaces = createRule('noSpaces', /^\S+$/, 'No spaces allowed');
const minTwelve = createRule('min12', /.{12,}/, 'At least 12 characters');Creating Rules as Objects
For complex validation logic that goes beyond regex:
import type { ValidationRule } from 'pwd-validator-react';
const notPassword: ValidationRule = {
name: 'notPassword',
message: 'Must not contain "password"',
validate: (pw) => !pw.toLowerCase().includes('password'),
};
const noConsecutive: ValidationRule = {
name: 'noConsecutive',
message: 'No 3 identical characters in a row',
validate: (pw) => !/(.)\1\1/.test(pw),
};Extending Default Rules
import { defaultRules, createRule } from 'pwd-validator-react';
const extendedRules = [
...defaultRules,
createRule('noSpaces', /^\S+$/, 'No spaces allowed'),
];
<PasswordField rules={extendedRules} />Using Only Specific Rules
import { minLength, hasUppercase, hasNumber } from 'pwd-validator-react';
const simpleRules = [minLength(6), hasUppercase(), hasNumber()];
<PasswordField rules={simpleRules} />matchesPassword
Rule factory for password confirmation validation. Accepts a getter function (instead of a direct string value) so the comparison target is read fresh on every validation:
import { matchesPassword, createDefaultRules, validatePassword } from 'pwd-validator-react';
function MyForm() {
const [password, setPassword] = useState('');
const [confirm, setConfirm] = useState('');
// Validate the confirm field against the main password:
const confirmRule = matchesPassword(() => password);
const confirmValid = confirmRule.validate(confirm);
return (
<>
<input value={password} onChange={(e) => setPassword(e.target.value)} />
<input value={confirm} onChange={(e) => setConfirm(e.target.value)} />
{!confirmValid && <p>Passwords do not match</p>}
</>
);
}The locale can be passed as the third argument to use the translated default message:
// Message from locale (confirmError):
matchesPassword(() => passwordValue, undefined, de)
// Custom message:
matchesPassword(() => passwordValue, 'Passwords do not match')Note:
PasswordFieldwithshowConfirmhandles this internally —matchesPasswordis intended for custom implementations.
Strength Calculation
Password strength is calculated by default using entropy (character pool × length) and rule compliance. The result is a value from 0–4.
How the Default Calculation Works
Determine the character pool — which character classes are present?
- Lowercase letters: +26
- Uppercase letters: +26
- Digits: +10
- Special characters: +32
Calculate entropy —
effectiveLength × log2(poolSize)effectiveLengthaccounts for repetition:"aaaaaaa"has 7 characters but only 1 unique character, so the effective length is reduced
Base score from entropy:
- < 25 bits → Level 1 (Weak)
- < 40 bits → Level 2 (Fair)
- < 60 bits → Level 3 (Strong)
- ≥ 60 bits → Level 4 (Very strong)
Rule cap — the score is capped when too few rules are satisfied:
- < 50 % satisfied → max Level 1
- < 75 % satisfied → max Level 2
- < 100 % satisfied → max Level 3
- 100 % satisfied → no cap
StrengthConfig
Use StrengthConfig to partially or fully customise the calculation:
import type { StrengthConfig } from 'pwd-validator-react';| Property | Type | Default | Description |
|----------|------|---------|-------------|
| entropyThresholds | [number, number, number] | [25, 40, 60] | Entropy thresholds in bits: [weak→fair, fair→strong, strong→very strong] |
| ruleCaps | [number, number, number] | [0.5, 0.75, 1.0] | Rule caps: score is limited when the fraction of satisfied rules falls below each threshold |
| minStrength | 0 \| 1 \| 2 \| 3 \| 4 | 2 | Minimum strength for isValid to be true |
| custom | (password: string, rules: ValidationRule[]) => PasswordStrength | — | Custom strength function (replaces the entire calculation) |
Adjusting Thresholds
// Stricter entropy requirements:
<PasswordField strengthConfig={{ entropyThresholds: [30, 50, 70] }} />
// More lenient rule caps:
<PasswordField strengthConfig={{ ruleCaps: [0.3, 0.5, 0.8] }} />
// Only valid at strength 3 or above:
<PasswordField strengthConfig={{ minStrength: 3 }} />
// Accept even empty passwords (for optional fields):
<PasswordField strengthConfig={{ minStrength: 0 }} />Custom Strength Function
The custom function replaces the entire entropy and rule logic. It receives the password and rules and must return a value from 0–4:
const myStrength: StrengthConfig = {
custom: (password, rules) => {
const passed = rules.filter((r) => r.validate(password)).length;
if (passed === rules.length && password.length >= 12) return 4;
if (passed === rules.length) return 3;
if (passed >= rules.length * 0.5) return 2;
return password.length > 0 ? 1 : 0;
},
};
<PasswordField strengthConfig={myStrength} />
// Works with the hook too:
usePasswordValidation({ strengthConfig: myStrength });
// And with the headless core:
getStrength('test', defaultRules, myStrength);When custom is set, entropyThresholds and ruleCaps are ignored. minStrength still applies to isValid.
Debounce
Delays validation while typing. The input value updates immediately, but strength calculation, validation messages, and callbacks wait for a pause in typing.
With PasswordField
<PasswordField debounceMs={300} />User types → the displayed value updates instantly → after a 300 ms pause the strength bar and messages update.
With the Hook
const { password, isValid, strength } = usePasswordValidation({ debounceMs: 300 });
// password updates immediately
// isValid and strength update after 300 ms of inactivityNotes
debounceMs: 0(default) = no delay, validation on every keystroke- The HIBP check has its own minimum debounce of 500 ms, independent of
debounceMs - Callbacks (
onStrengthChange,onValidChange) fire after the debounce
Callbacks
React to changes in strength, validity, or match status. All callbacks fire only when the value actually changes. With fireOnMount: true they also fire once immediately on mount.
With PasswordField
function MyForm() {
const [strength, setStrength] = useState<PasswordStrength>(0);
const [valid, setValid] = useState(false);
return (
<>
<PasswordField
onStrengthChange={(s) => setStrength(s)}
onValidChange={(v) => setValid(v)}
/>
<p>Current strength: {strength}</p>
<p>Valid: {valid ? 'Yes' : 'No'}</p>
</>
);
}With the Hook
const validation = usePasswordValidation({
onStrengthChange: (s) => {
// called when strength changes (e.g. 1 → 2)
// NOT on mount, NOT when the value stays the same
console.log('New strength:', s);
},
onValidChange: (v) => {
// called when validity changes (e.g. false → true)
console.log('Valid:', v);
},
});Behaviour
- Callbacks are kept with stable references (via
useRef), so inline arrow functions do not cause unnecessary re-renders - With
fireOnMount: true,onStrengthChangeandonValidChangefire once on mount with the initial value (strength0,isValid: falsefor an empty password) - When debounce is active, callbacks fire after the debounced value settles
onMatchChange(only withshowConfirm) fires when the match status between password and confirmation changes
fireOnMount
With fireOnMount: true, onStrengthChange and onValidChange fire once when the component mounts — even without any user input. Useful when the parent form needs to know the initial state.
With PasswordField
function MyForm() {
const [valid, setValid] = useState<boolean>(false);
return (
<PasswordField
fireOnMount
onValidChange={(v) => setValid(v)}
/>
);
// valid is immediately `false` — no waiting for the first keystroke
}With the Hook
const validation = usePasswordValidation({
fireOnMount: true,
onStrengthChange: (s) => console.log('Initial strength:', s), // 0
onValidChange: (v) => console.log('Initially valid:', v), // false
});Behaviour
onStrengthChangeandonValidChangeare called on the first render with the initial valueonMatchChangeis not affected byfireOnMount- Without
fireOnMount(default), callbacks fire only on actual value changes
Zod Integration
toZod(rules) converts an array of ValidationRule objects into a Zod schema. The entry point is tree-shakable and requires zod >= 4.0.0 as a peer dependency.
npm install zodimport { toZod } from 'pwd-validator-react/zod';
import { createDefaultRules } from 'pwd-validator-react';
import { de } from 'pwd-validator-react/locales';
const rules = createDefaultRules(de);
const schema = toZod(rules);
// Single check:
const result = schema.safeParse('MyP@ssw0rd');
if (result.success) {
console.log('Valid!');
} else {
result.error.issues.forEach((i) => console.log(i.message));
// "Mindestens 8 Zeichen", "Mindestens 1 Großbuchstabe", ...
}With react-hook-form
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { toZod } from 'pwd-validator-react/zod';
import { createDefaultRules } from 'pwd-validator-react';
import { z } from 'zod';
const schema = z.object({
password: toZod(createDefaultRules()),
});
const { register, handleSubmit } = useForm({
resolver: zodResolver(schema),
});All Failing Rules Are Reported
When a password is invalid, error.issues contains one entry per failing rule — not just the first:
const result = schema.safeParse('abc');
// result.error.issues = [
// { message: 'Mindestens 8 Zeichen' },
// { message: 'Mindestens 1 Großbuchstabe' },
// { message: 'Mindestens 1 Zahl' },
// { message: 'Mindestens 1 Sonderzeichen' },
// ]Note:
zodmust be installed separately (npm install zod).pwd-validator-reactlists it as an optional peer dependency.
Password Confirmation
Optional second field for password confirmation. The error message is only shown after the user has interacted with the confirmation field.
With PasswordField
// Default texts (English):
<PasswordField showConfirm />
// With locale:
import { de } from 'pwd-validator-react/locales';
<PasswordField showConfirm locale={de} />
// Custom texts:
<PasswordField
showConfirm
confirmLabel="Repeat password"
confirmError="Passwords do not match"
/>
// With callback:
<PasswordField
showConfirm
onMatchChange={(matches) => {
if (matches) console.log('Passwords match!');
}}
/>The confirmation field automatically inherits the variant of the main input (floating or classic).
With the Hook
const {
password, setPassword,
confirmPassword, setConfirmPassword,
passwordsMatch,
} = usePasswordValidation({ showConfirm: true });
// passwordsMatch is true when:
// - showConfirm is false (default), OR
// - password === confirmPasswordBehaviour
- The error message is only shown after the confirmation field has been touched (
confirmTouched) - When debounce is active, the comparison runs against the debounced password value
- The confirmation field has
autoComplete="new-password"for correct password manager support
Overlay Mode
Shows the strength bar and validation messages in a floating popup overlay instead of inline below the input. The overlay opens on the first keystroke and closes on blur.
// Overlay active:
<PasswordField overlayMode locale={de} showToggle />
// With custom width:
<PasswordField overlayMode popupWidth={320} />
<PasswordField overlayMode popupWidth="120%" />
// With callbacks:
<PasswordField
overlayMode
onOverlayOpen={() => console.log('Overlay opened')}
onOverlayClose={() => console.log('Overlay closed')}
/>Behaviour
- The overlay opens on the first keystroke (not on mere focus)
- The overlay positions itself automatically above or below the input depending on available space
- In overlay mode the strength label is hidden by default (
showStrengthLabeldefaults tofalsein overlay,trueinline) — override withshowStrengthLabel={true} - The overlay has a fade-in/out animation (
opacity+translateY, controlled via--pvr-transition-duration) onOverlayOpenfires on the first keystroke when focused;onOverlayClosefires on blur
Combined with Generator
<PasswordField overlayMode showGenerator showToggle />Password Generator
Generates cryptographically secure passwords via crypto.getRandomValues. Available as an exported function and as an optional UI button in PasswordField.
Exported Function
import { generatePassword } from 'pwd-validator-react';
import type { GeneratePasswordOptions } from 'pwd-validator-react';
// Default: 16 characters, all character classes:
const pw = generatePassword();
// With length:
const pw = generatePassword({ length: 24 });
// Matched to active rules (recommended):
import { createDefaultRules } from 'pwd-validator-react';
const pw = generatePassword({ rules: createDefaultRules() });
// Custom charset:
const pw = generatePassword({
length: 20,
charset: {
uppercase: true,
lowercase: true,
numbers: true,
special: '!@#$%', // custom special characters, or false to disable
},
});GeneratePasswordOptions
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| length | number | max(minLength rule, 16) | Password length |
| rules | ValidationRule[] | [] | Rules to derive length and charset from |
| charset.uppercase | boolean | true (no rules) / derived | Uppercase letters |
| charset.lowercase | boolean | true | Lowercase letters |
| charset.numbers | boolean | true (no rules) / derived | Digits |
| charset.special | string \| false | '!@#$%^&*' (no rules) / derived | Special characters string or false to disable |
Note: When
rulesare provided, the function derives the charset from the rule names (hasUppercase,hasNumber,hasSpecialChar). Without rules, all character classes are enabled by default.
UI Button in PasswordField
// Show the generator button:
<PasswordField showGenerator />
// Combined with overlay mode:
<PasswordField showGenerator overlayMode />When the button is clicked:
- A new password is generated (matched to the active rules)
- The input field is populated
- Validation runs immediately
onChangeis called (for controlled-mode consumers)
Have I Been Pwned (HIBP)
Checks the password against the HaveIBeenPwned database of known data breaches.
Security
The check uses k-Anonymity: the password is hashed locally with SHA-1, and only the first 5 of 40 hex characters are sent to the API. Comparison of the full hash happens exclusively in the browser. The Add-Padding: true header prevents traffic analysis.
The plaintext password never leaves the client.
With PasswordField
<PasswordField checkPwned />
// Custom warning text:
<PasswordField checkPwned pwnedMessage="This password is compromised!" />
// Recommended: add debounce for better UX:
<PasswordField checkPwned debounceMs={500} />When a password is found, a warning message (role="alert") appears below the validation messages.
With the Hook
const {
isPwned, // true | false | null
pwnedCount, // number of breach occurrences
isPwnedLoading, // true while the check is running
} = usePasswordValidation({ checkPwned: true });
// isPwned states:
// null = not yet checked, empty password, or API error
// false = not found in any breach
// true = found in a breachHeadless (without React)
import { checkPwned } from 'pwd-validator-react/hibp';
try {
const { isPwned, count } = await checkPwned('myPassword');
if (isPwned) {
console.log(`Password found in ${count} breaches!`);
}
} catch (error) {
console.error('HIBP check failed:', error);
}Important:
checkPwnedmust be imported frompwd-validator-react/hibp, not from the main entry point.
Notes
- The API request is automatically debounced to at least 500 ms, even if
debounceMsis lower - Ongoing requests are cancelled via
AbortControllerwhen the user types quickly - API errors set
isPwnedtonull(notfalse) — an error is not treated as "safe" - Empty passwords do not trigger a check
Styling & Theming
Importing the Stylesheet
import 'pwd-validator-react/styles.css';Without the Stylesheet
The components work without the CSS import. You can style the BEM classes (pvr-input-wrapper, pvr-strength-bar, pvr-messages, etc.) yourself, or add your own classes via className.
Overriding CSS Variables
All styles are based on CSS custom properties. Override them globally or scoped to a container:
/* Global: */
:root {
--pvr-focus-color: #8b5cf6;
--pvr-error-color: #ef4444;
--pvr-border-radius: 12px;
--pvr-strength-weak: #ef4444;
--pvr-strength-fair: #eab308;
--pvr-strength-good: #22c55e;
--pvr-strength-strong: #16a34a;
}
/* Scoped to a container: */
.dark-theme {
--pvr-bg-color: #1f2937;
--pvr-text-color: #f9fafb;
--pvr-border-color: #4b5563;
--pvr-label-color: #9ca3af;
--pvr-strength-bg: #374151;
}All CSS Variables
| Variable | Default | Description |
|----------|---------|-------------|
| --pvr-font-family | inherit | Font family |
| --pvr-font-size | 1rem | Font size |
| --pvr-line-height | 1.5 | Line height |
| --pvr-border-color | #d1d5db | Input border colour |
| --pvr-border-width | 1.5px | Input border width |
| --pvr-border-radius | 8px | Border radius |
| --pvr-focus-color | #2563eb | Focus ring and focused label colour |
| --pvr-error-color | #dc2626 | Error state colour (border, messages, warnings) |
| --pvr-success-color | #16a34a | Passing rule checkmark colour |
| --pvr-bg-color | #ffffff | Input background colour |
| --pvr-text-color | #111827 | Input text colour |
| --pvr-placeholder-color | #9ca3af | Placeholder colour (classic variant only) |
| --pvr-label-color | #6b7280 | Label colour (unfocused) |
| --pvr-toggle-color | #6b7280 | Visibility toggle icon colour |
| --pvr-toggle-hover-color | #374151 | Visibility toggle hover colour |
| --pvr-strength-weak | #dc2626 | Strength bar — weak (red) |
| --pvr-strength-fair | #f59e0b | Strength bar — fair (orange) |
| --pvr-strength-good | #16a34a | Strength bar — strong (green) |
| --pvr-strength-strong | #15803d | Strength bar — very strong (dark green) |
| --pvr-strength-bg | #e5e7eb | Strength bar — inactive segment |
| --pvr-transition-duration | 200ms | Animation duration |
| --pvr-spacing-xs | 0.25rem | Extra-small spacing unit |
| --pvr-spacing-sm | 0.5rem | Small spacing unit |
| --pvr-spacing-md | 0.75rem | Medium spacing unit |
BEM Classes
Components use BEM classes with the pvr- prefix:
| Class | Description |
|-------|-------------|
| .pvr-field | Wrapper around the entire PasswordField |
| .pvr-input-wrapper | Wrapper around PasswordInput |
| .pvr-input-wrapper--floating | Floating variant |
| .pvr-input-wrapper--classic | Classic variant |
| .pvr-input-wrapper--has-toggle | When the toggle is visible |
| .pvr-input-field | The <input> element |
| .pvr-input-field--error | Input in error state |
| .pvr-input-label | The label |
| .pvr-input-toggle | The visibility toggle button |
| .pvr-strength-bar | Strength bar container |
| .pvr-strength-bar--strength-{0-4} | Modifier for the current level |
| .pvr-strength-bar__segment | Individual segment (4 total) |
| .pvr-strength-bar__label | Text label below the bar |
| .pvr-messages | Validation messages container |
| .pvr-messages__list | The <ul> list |
| .pvr-messages__item | Individual message |
| .pvr-messages__item--error | Failing rule (red) |
| .pvr-messages__item--valid | Passing rule (green) |
| .pvr-messages__item__icon | Checkmark/cross icon |
| .pvr-confirm | Wrapper around the confirmation field |
| .pvr-confirm-error | Error message on mismatch |
| .pvr-pwned-warning | HIBP warning |
| .pvr-overlay | Overlay popup container |
| .pvr-overlay--below | Overlay opens below the input |
| .pvr-overlay--above | Overlay opens above the input |
| .pvr-overlay--open | Overlay is visible (animation active) |
| .pvr-generator-btn | Password generator button |
| .pvr-strength-bar--no-label | Strength bar without label (when showStrengthLabel={false}) |
Accessibility
The components implement the following accessibility features:
aria-invalidon the input when there are validation errorsaria-describedbylinks the input to the validation messages and confirmation errorrole="alert"+aria-live="assertive"on validation messages for screen reader announcementsrole="alert"on the confirmation error and the HIBP warningrole="meter"witharia-valuenow,aria-valuemin,aria-valuemax, andaria-labelon the strength bararia-labelon the visibility toggle (localised vialocale.showPassword/locale.hidePassword)- Touch targets of at least 44×44 px (WCAG 2.5.5)
focus-visiblestyles for keyboard navigation- Input font of at least 16 px on mobile (prevents automatic zoom on iOS)
- Semantic HTML with
<label>,htmlFor, and automatically generated IDs
Next.js
All components and the hook use the "use client" directive and are fully compatible with the Next.js App Router:
// app/registration/page.tsx
import { PasswordField } from 'pwd-validator-react';
import 'pwd-validator-react/styles.css';
export default function RegistrationPage() {
return (
<form>
<PasswordField showToggle showConfirm checkPwned debounceMs={300} />
<button type="submit">Register</button>
</form>
);
}The core functions (validatePassword, getStrength, defaultRules, createDefaultRules) are server-compatible and can be used in Server Components, API Routes, or Middleware:
// app/api/validate/route.ts
import { validatePassword } from 'pwd-validator-react';
export async function POST(req: Request) {
const { password } = await req.json();
const result = validatePassword(password);
return Response.json(result);
}checkPwned can also be used server-side — import it from pwd-validator-react/hibp:
// app/api/check-pwned/route.ts
import { checkPwned } from 'pwd-validator-react/hibp';
export async function POST(req: Request) {
const { password } = await req.json();
const result = await checkPwned(password);
return Response.json(result);
}Controlled vs. Uncontrolled
Uncontrolled (default)
PasswordField manages its own state internally:
<PasswordField
onChange={(e) => {
// You receive the value, but do not need to store it yourself
console.log(e.target.value);
}}
/>Controlled
You control the value entirely:
function MyForm() {
const [password, setPassword] = useState('');
return (
<PasswordField
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
);
}In controlled mode PasswordField only updates the displayed value when value changes. Internal validation still runs automatically.
Hook
The hook is always "uncontrolled" — it manages its own state:
const { password, setPassword } = usePasswordValidation();
// setPassword('newValue') sets the value and marks as touchedTypeScript Types
All types are exported and can be imported directly:
import type {
ValidationRule, // { name: string; message: string; validate: (pw: string) => boolean }
ValidationResult, // { isValid: boolean; errors: ValidationError[]; strength: PasswordStrength }
ValidationError, // { rule: string; message: string }
PasswordStrength, // 0 | 1 | 2 | 3 | 4
StrengthConfig, // { entropyThresholds?, ruleCaps?, minStrength?, custom? }
PvrLocale, // see below
PasswordInputProps, // props for <PasswordInput>
StrengthBarProps, // props for <StrengthBar>
ValidationMessagesProps, // props for <ValidationMessages>
PasswordFieldProps, // props for <PasswordField>
UsePasswordValidationOptions, // options for usePasswordValidation()
UsePasswordValidationReturn, // return type of usePasswordValidation()
GeneratePasswordOptions, // options for generatePassword()
} from 'pwd-validator-react';PvrLocale
interface PvrLocale {
// Validation rule messages (functions because they can be parameterised)
minLength: (min: number) => string;
maxLength: (max: number) => string;
hasUppercase: (min: number) => string;
hasLowercase: (min: number) => string;
hasNumber: (min: number) => string;
hasSpecialChar: (min: number) => string;
// PasswordInput
label: string;
showPassword: string;
hidePassword: string;
// StrengthBar
strengthLabel: string;
strengthLevels: string[]; // 4 entries: [weak, fair, strong, very strong]
// PasswordField
confirmLabel: string;
confirmError: string;
pwnedMessage: string;
pwnedChecking: string; // shown while the HIBP check is in progress
generatePassword: string; // label for the generator button
// Direction (optional)
dir?: 'ltr' | 'rtl';
}Development
# Install dependencies
npm install
# Start the dev server (Vite demo at http://localhost:5173)
npm run dev
# Run tests (263 tests)
npm test
# Run tests in watch mode
npm run test:watch
# TypeScript type check
npm run typecheck
# Production build (ESM + CJS + DTS + CSS)
npm run buildProject Structure
src/
core/ # Layer 1: pure functions (no React)
types.ts # TypeScript types
rules.ts # validation rules + createRule() + createDefaultRules()
validate.ts # validatePassword() + getStrength()
hibp.ts # checkPwned() (k-Anonymity)
generate.ts # generatePassword() — cryptographically secure password generator
locale.ts # PvrLocale interface + en locale + defineLocale()
locales/ # Separate entry point: pwd-validator-react/locales
de.ts fr.ts it.ts nl.ts es.ts tr.ts he.ts ar.ts pt.ts pl.ts ja.ts
index.ts # re-exports all locales
hooks/ # Layer 2: React hooks
usePasswordValidation.ts
useDebouncedValue.ts (internal, not exported)
components/ # Layer 3: React components
PasswordInput.tsx
StrengthBar.tsx
ValidationMessages.tsx
PasswordField.tsx
icons.tsx
hibp.ts # Separate entry point for checkPwned()
locales.ts # Separate entry point for locales (re-exports locales/)
index.ts # Barrel exports (main entry point)
styles/ # SCSS with CSS custom properties
_variables.scss
_input.scss
_strength-bar.scss
_messages.scss
index.scss
demo/ # Vite demo appLicense
MIT © 2026 Peter R. Stuhlmann
