use-password-policy
v3.1.0
Published
Password validation for React and Node: one policy for client and server, NIST presets, breached-password checks, zxcvbn support, Zod & react-hook-form helpers, and an accessible drop-in input.
Maintainers
Readme
use-password-policy
Write your password rules once. Use them in your React form and on your server.
- A hook and an accessible drop-in
<PasswordPolicyInput /> - A framework-free
validatePassword()for Node, edge functions and API routes - A NIST SP 800-63B preset, a common-password blocklist and Have I Been Pwned breach checks
- Optional zxcvbn scoring, so "strength" means how hard a password is to guess, not how many boxes it ticks
- Zod and react-hook-form helpers
- No runtime dependencies. About 4 KB gzipped for the core, about 6.5 KB with the React parts.
➡️ Live demo & playground

Install
npm install use-password-policyReact 16.8+ is needed for the hook and component. It is tested on React 18 and 19. The use-password-policy/core entry doesn't need React at all.
Quick start
1. Drop-in component
import { PasswordPolicyInput } from 'use-password-policy';
function SignUp() {
const [isValid, setIsValid] = useState(false);
return (
<form>
<label htmlFor="password">Password</label>
<PasswordPolicyInput
id="password"
name="password"
policyOptions={{ minLength: 10 }}
onPasswordChange={(_, v) => setIsValid(v.isValid)}
/>
<button disabled={!isValid}>Sign up</button>
</form>
);
}The component comes with its own styles, a strength meter, a checklist, and a show/hide button that screen readers can use.
2. Hook (build your own UI)
import { usePasswordPolicy } from 'use-password-policy';
const { isValid, requirements, strengthLabel, strengthPercent } = usePasswordPolicy({
password,
minLength: 10,
customRules: [{ name: 'noSpaces', message: 'No spaces', test: (p) => !/\s/.test(p) }],
});
<ul>
{requirements.map((r) => (
<li key={r.name} style={{ color: r.passed ? 'green' : 'crimson' }}>{r.message}</li>
))}
</ul>3. The same policy on the server
// password-policy.ts — shared by client and server
import { presets, type PasswordPolicyOptions } from 'use-password-policy/core';
export const policy: PasswordPolicyOptions = { ...presets.nist, breachCheck: true };// api/sign-up.ts (Node, Next.js route handler, Express, Cloudflare Worker…)
import { validatePasswordAsync } from 'use-password-policy/core';
import { policy } from './password-policy';
const { isValid, errors } = await validatePasswordAsync(body.password, policy);
if (!isValid) return Response.json({ errors }, { status: 400 });Use validatePasswordAsync when the policy has breachCheck. Without it, the synchronous validatePassword returns the same result.
use-password-policy/core doesn't import React, so it's safe in server bundles.
Presets
import { presets } from 'use-password-policy';
usePasswordPolicy({ ...presets.nist, password }); // NIST, password used on its own
usePasswordPolicy({ ...presets.nistMfa, password }); // NIST, password is one factor of MFA
usePasswordPolicy({ ...presets.classic, password }); // 8+ chars, upper, lower, number, symbol (the default)| Preset | Min | Max | Composition rules | Blocks common passwords | Blocks patterns |
| --- | --- | --- | --- | --- | --- |
| classic (default) | 8 | – | upper, lower, number, symbol | no | no |
| nist | 15 | 64 | none | yes | yes |
| nistMfa | 8 | 64 | none | yes | yes |
The NIST presets follow SP 800-63B-4. It asks for length and a blocklist check, and says not to require "mixtures of different character types". Length is counted in Unicode code points, as NIST specifies, so an emoji counts as one character. To also check known breaches, add breachCheck: true.
Security add-ons
Block common passwords
usePasswordPolicy({ password, commonPasswordCheck: true });This uses a small built-in list of the most common passwords and base words. It also catches simple variations such as Password123!, P@ssw0rd, 123qwerty and Monkey!!, and list words joined together such as passwordpassword or Summer2024!Summer. Pass commonPasswords: [...] to use your own list, for example your product name.
Block predictable patterns
usePasswordPolicy({ password, patternCheck: true }); // on in the NIST presetsRejects repeated characters (aaaaaaaaaaaaaaa), repeated chunks (qwertyqwertyqwerty, dragon dragon dragon), sequences and keyboard runs (123456789012345, abcdefghijk, qwertyuiop), and passwords made of only a few distinct characters, including all spaces.
Check breached passwords (Have I Been Pwned)
const { isValid, requirements, breach } = usePasswordPolicy({ ...presets.nist, breachCheck: true, password });With breachCheck, the check is part of the normal result:
- Once every other rule passes, the hook checks Have I Been Pwned (debounced, stale requests cancelled).
- A "Not found in known data breaches" requirement is
pendinguntil the check answers, andisValidstaysfalseuntil then. - A breached password fails with that message and is marked Very Weak.
breachgives{ status, count }, e.g.{ status: 'pwned', count: 10434004 }. <PasswordPolicyInput policyOptions={{ breachCheck: true }} />shows it in the checklist automatically.
On the server, await validatePasswordAsync(password, policy) does the same.
If the service can't be reached, the password is let through by default and breach.status is 'error'. Pass breachCheck: { failOpen: false } to block instead. Other settings: debounceMs (default 500), fetch, endpoint, padding.
For a custom flow, the lower-level usePwnedPassword(password) hook and checkPwnedPassword(password) (returns the breach count) are still available.
Only the first 5 characters of the password's SHA-1 hash are sent (k-anonymity), never the password. It needs crypto.subtle, which means HTTPS or localhost in browsers and Node 20+ on the server.
Real strength scoring with zxcvbn
A checklist can't tell Password1! apart from a strong password. zxcvbn can. Add it yourself (it's large, so it isn't bundled) and wrap it with fromZxcvbn:
import { ZxcvbnFactory } from '@zxcvbn-ts/core';
import * as common from '@zxcvbn-ts/language-common';
import * as en from '@zxcvbn-ts/language-en';
import { fromZxcvbn } from 'use-password-policy';
const zxcvbn = new ZxcvbnFactory({
dictionary: { ...common.dictionary, ...en.dictionary },
graphs: common.adjacencyGraphs,
translations: en.translations,
});
const strengthEstimator = fromZxcvbn(zxcvbn); // create once, outside your component
usePasswordPolicy({ password, strengthEstimator, minStrength: 3 });With an estimator, strengthLabel and strengthPercent come from its score (0–4). minStrength adds a "Hard to guess" requirement, and estimate.feedback gives you a hint to show the user. fromZxcvbn also accepts a plain function, such as the original zxcvbn package.
Form libraries
import { z } from 'zod';
import { zodPasswordRule, passwordValidator } from 'use-password-policy/core';
// Zod 3 or 4: one issue per failed rule
const schema = z.object({ password: z.string().superRefine(zodPasswordRule(policy)) });
// react-hook-form: returns true or the first error message
register('password', { validate: passwordValidator(policy) });With breachCheck, use the async versions, zodPasswordRuleAsync(policy) (with parseAsync) and passwordValidatorAsync(policy), so the breach check runs too. None of these helpers import Zod or react-hook-form, so they add no dependencies.
Confirm-password field
usePasswordPolicy({ password, confirmPassword }); // adds a "Passwords match" requirementCustom messages & i18n
Every requirement has a readable message. You can override any of them with a string or a function:
usePasswordPolicy({
password,
messages: {
minLength: (o) => `Mindestens ${o.minLength} Zeichen`,
uppercase: 'Ein Großbuchstabe',
},
});Rule names: minLength, maxLength, uppercase, lowercase, number, specialChar, notCommon, match, strength, plus your custom rule names.
API
Options (PasswordPolicyOptions)
| Option | Type | Default | Description |
| --- | --- | --- | --- |
| password | string | '' | Password to check (hook only). |
| minLength | number | 8 | Minimum length. 0 turns it off. |
| maxLength | number | 0 | Maximum length. 0 means no maximum. |
| lowercaseCheck | boolean | true | Require a lowercase letter. |
| uppercaseCheck | boolean | true | Require an uppercase letter. |
| numberCheck | boolean | true | Require a digit. |
| specialCharCheck | boolean | true | Require a special character. |
| commonPasswordCheck | boolean | false | Reject common passwords. |
| patternCheck | boolean | false | Reject repeats, sequences and keyboard patterns. |
| breachCheck | boolean \| { failOpen, debounceMs, fetch, endpoint, padding } | false | Include the Have I Been Pwned check (hook and validatePasswordAsync). |
| commonPasswords | string[] | built-in | Replace the blocklist. |
| confirmPassword | string | – | Adds a match rule when set. |
| strengthEstimator | (pw) => { score, feedback? } | – | For example fromZxcvbn(zxcvbn). |
| minStrength | 0–4 | – | With an estimator: minimum score required. |
| customRules | PolicyRule[] | [] | { name, test, message? } |
| messages | Record<string, string \| (o) => string> | – | Override requirement text. |
| lowercaseRegex / uppercaseRegex / numberRegex / specialCharRegex | RegExp | – | Change what counts as each character type. |
Result (hook and validatePassword)
| Key | Type | Description |
| --- | --- | --- |
| isValid | boolean | true only when every active rule passes. |
| requirements | { name, passed, message }[] | Ordered checklist, ready to render. |
| errors | string[] | Messages of the failed rules. |
| policyState | Record<string, boolean> | Pass/fail by rule name. |
| strengthLabel | 'Very Weak' \| 'Weak' \| 'Medium' \| 'Strong' \| 'Very Strong' | |
| strengthPercent | number (0–1) | Fill for a meter. |
| strengthScore | number | Number of rules passed. |
| estimate | { score, feedback? } | Only with strengthEstimator. |
| breach | { status, count } | Only with breachCheck. status is idle, checking, safe, pwned or error. |
Each requirement is { name, passed, message, pending? }. pending is true while the breach check hasn't answered.
<PasswordPolicyInput /> props
It accepts every normal <input> prop (id, name, placeholder, autoComplete, onBlur, and so on) and forwards ref to the input. It also takes:
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| policyOptions | PasswordPolicyOptions | {} | Same options as the hook. |
| onPasswordChange | (password, validation) => void | – | Called on every change with the fresh result. |
| value / defaultValue | string | – | Controlled or uncontrolled. onChange works as usual. |
| showStrengthMeter | boolean | true | |
| showStrengthLabel | boolean | false | Shows the label ("Strong") under the meter. |
| showRequirementsList | boolean | true | |
| showToggleButton | boolean | true | Show/hide password button. |
| toggleLabels | { show, hide } | Show password / Hide password | Accessible labels for the button. |
| className | string | – | Class on the wrapper. |
| inputClassName | string | – | Class on the <input>. |
| unstyled | boolean | false | Leaves out the built-in CSS. |
Accessibility: the checklist is linked to the input with aria-describedby, the meter has role="meter", aria-invalid is set once the user types an invalid password, and each item announces "met" or "not met".
Styling
The component ships plain CSS with no CSS-in-JS. Theme it with CSS variables from any class:
.my-password {
--rpp-accent: #0ea5e9;
--rpp-success: #16a34a;
--rpp-danger: #dc2626;
--rpp-weak: #ea580c;
--rpp-medium: #ca8a04;
--rpp-bg: #fff;
--rpp-border: #d4d4d8;
--rpp-text: #18181b;
--rpp-muted: #71717a;
--rpp-radius: 8px;
}<PasswordPolicyInput className="my-password" />Each part has a stable class you can target: .rpp-root, .rpp-input, .rpp-toggle, .rpp-meter, .rpp-segment, .rpp-requirements, .rpp-requirement (with [data-passed]). The built-in selectors have low specificity, so .my-password .rpp-input { … } always wins. This works with Tailwind, CSS Modules and styled-components (styled(PasswordPolicyInput) still works).
To use your own stylesheet instead of the built-in one, pass unstyled and optionally start from the shipped file: import 'use-password-policy/styles.css'.
For Next.js App Router, the React entry is marked 'use client', and use-password-policy/core can be used in Server Components and route handlers.
Upgrading from v2
styled-componentsis no longer required. You can uninstall it if nothing else in your app uses it.- The component's DOM and class names changed (
.rpp-*). The--rpp-*theme variables still work. onPasswordChangenow runs on user changes only, not on mount.- The hook's options and return values are backward compatible. New fields were added:
requirements,errors,strengthPercent,estimate.
See the CHANGELOG.
Contributing
Issues and PRs are welcome. To get started:
npm install
npm test # vitest
npm run build # tsup
npm run dev -w demoLicense
MIT © Rahul Patwa
