Examples โ
Interactive examples and code snippets for common use cases.
Live Examples โ
Interactive Playground โ
Try out the interactive playground to test password validation in real time:
๐ Open Playground
The playground features:
- Live password validation as you type
- Policy controls (
minLength,requireUppercase/Lowercase/Digit/Symbol) forwarded viavalidatorOptions - English/Spanish validation messages via the i18n
messagestemplate map - Toggles for
PasswordInput's behavior knobs (validate on mount/change, debounce delay, visibility of the toggle button and validation messages) - Visual strength meter (label + numeric
0โ4score) - Per-check pass/fail grid (length, character types, repetition, sequential, keyboard pattern, common password, personal info)
- Entropy bits, 0โ4 entropy score, and crack-time estimates under four attacker models
The playground exposes policy controls (
minLength, the fourrequire*toggles) and an English/Spanish message-language switch demonstrating the i18nmessagestemplate map, alongside the component-behavior knobs. It also shows live entropy bits and crack-time estimates from@sentinel-password/entropy. For options it doesn't surface (personalInfo,formatMessage, thresholds), pass them viavalidatorOptionson the component or drive a plain<input>fromusePasswordValidator.
Working Examples โ
Complete, runnable examples in different stacks:
- Next.js 16 Example โ Signup form with App Router and Tailwind CSS ยท source ยท run in StackBlitz
- Vite + React Example โ Signup form with custom CSS styling ยท source ยท run in StackBlitz
- Express Backend Example โ Server-side
/signupvalidation with Express 5 ยท source ยท run in StackBlitz. See the Server-Side Usage guide for Fastify, NestJS, and edge-runtime variants. - Playground โ Interactive component configurator (hosted, no setup)
Running an example
The StackBlitz links open each example in an online IDE. The example package.json files reference the monorepo via workspace:* โ if the online install trips on that protocol, replace those entries with latest (they track the published @sentinel-password/* packages). To run locally, clone the monorepo, pnpm install, pnpm build, then pnpm --filter <example> dev from the repo root โ an example folder downloaded on its own will not install.
Quick Links โ
Basic Validation โ
import { validatePassword } from '@sentinel-password/core'
const result = validatePassword('Quartz-Glider!9pump', {
minLength: 8,
maxLength: 128,
requireUppercase: true,
requireLowercase: true,
requireDigit: true,
requireSymbol: true,
})
console.log(result)
// {
// valid: true,
// score: 4,
// strength: 'very-strong',
// feedback: { suggestions: [] },
// checks: { length: true, characterTypes: true, ... },
// }Common pitfall
Suffixes like 123, abc, or qwerty trigger the sequential and keyboard-pattern checks even when the rest of the password looks strong. MySecureP@ss123 looks plausible but fails โ it satisfies every character-type requirement and returns valid: false with strength: 'strong'. The strength label and the valid flag aren't redundant: valid requires every check to pass; strength reflects the ratio.
React Form โ
A signup form using usePasswordValidator:
import { useState } from 'react'
import { usePasswordValidator } from '@sentinel-password/react'
function SignupForm() {
const [email, setEmail] = useState('')
const [submitted, setSubmitted] = useState(false)
const { password, setPassword, result } = usePasswordValidator({
minLength: 8,
requireUppercase: true,
requireDigit: true,
})
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
// Defense in depth: the submit button is disabled when the password is
// invalid, but keyboard-Enter inside an input bypasses `disabled`. The
// inline suggestions and aria-invalid below already announce the failure
// โ no alert() needed.
if (!result?.valid) return
await fetch('/api/signup', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ email, password }),
})
setSubmitted(true)
}
if (submitted) return <p>Account created successfully!</p>
return (
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="email">Email</label>
<input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
</div>
<div>
<label htmlFor="password">Password</label>
<input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
aria-invalid={result && !result.valid ? true : undefined}
/>
{result?.feedback.suggestions.map((suggestion, i) => (
<p key={i} style={{ color: 'red' }}>
{suggestion}
</p>
))}
{result && (
<p>
Strength: <strong>{result.strength}</strong>
</p>
)}
</div>
<button type="submit" disabled={!result?.valid || !email}>
Sign Up
</button>
</form>
)
}Custom Styling โ
PasswordInput is fully headless. Each subtree gets its own class name prop:
import { PasswordInput } from '@sentinel-password/react-components'
function StyledForm() {
return (
<div className="max-w-md mx-auto p-6">
<PasswordInput
label="Create Password"
description="At least 8 characters"
containerClassName="mb-6"
labelClassName="block text-sm font-semibold text-gray-900 mb-2"
descriptionClassName="text-sm text-gray-600 mb-3"
inputWrapperClassName="relative"
className="w-full px-4 py-3 pr-24 border-2 border-gray-300 rounded-lg focus:border-blue-500 focus:ring-4 focus:ring-blue-100 transition-all"
toggleButtonClassName="absolute right-2 top-1/2 -translate-y-1/2 px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-100 rounded-md"
validationClassName="mt-3 space-y-2"
/>
</div>
)
}Configure validation via validatorOptions
The component runs the built-in defaults (minLength: 8, all check* flags on, no required character types) unless you pass validatorOptions. That prop forwards to every internal validatePassword(...) call โ covering minLength, requireUppercase, personalInfo, plus the v1.2.0 i18n options messages and formatMessage. Memoize the object if it contains closures (e.g. formatMessage); the component bails out of state updates when re-validation produces a semantically identical result, so identity churn won't loop, but it still re-runs validatePassword on each new reference.
Real-time Feedback โ
A strength bar driven by the hook's result.score (0โ4):
import { usePasswordValidator } from '@sentinel-password/react'
function PasswordWithStrength() {
const { password, setPassword, result } = usePasswordValidator({
minLength: 8,
requireUppercase: true,
requireDigit: true,
requireSymbol: true,
})
const score = result?.score ?? 0
const widthPct = ((score + 1) / 5) * 100
const color =
score <= 1 ? 'bg-red-500' : score === 2 ? 'bg-yellow-500' : 'bg-green-500'
return (
<div>
<label htmlFor="pw">Password</label>
<input
id="pw"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<div className="w-full h-2 bg-gray-200 rounded-full mt-2">
<div
className={`h-full rounded-full transition-all ${color}`}
style={{ width: `${widthPct}%` }}
/>
</div>
<p className="mt-1 text-sm text-gray-600">
Strength: <span className="font-semibold">{result?.strength ?? 'โ'}</span>
</p>
{result && !result.valid && (
<ul className="mt-2 space-y-1">
{result.feedback.suggestions.map((msg, i) => (
<li key={i} className="text-sm text-red-600">
โ {msg}
</li>
))}
</ul>
)}
{result?.valid && password && (
<p className="mt-2 text-sm text-green-600">โ Password meets all requirements</p>
)}
</div>
)
}Multi-step Forms โ
For personalInfo to do its job, every identifier you want to reject from the password must be collected before the password step. Below, email and name come first, then the validator can reject passwords containing either when the user reaches the password screen.
import { useState } from 'react'
import { usePasswordValidator } from '@sentinel-password/react'
type Step = 'email' | 'profile' | 'password' | 'complete'
function MultiStepSignup() {
const [step, setStep] = useState<Step>('email')
const [email, setEmail] = useState('')
const [name, setName] = useState('')
const { password, setPassword, result } = usePasswordValidator({
minLength: 8,
requireUppercase: true,
requireDigit: true,
personalInfo: [email, name].filter(Boolean),
})
const next = () => {
if (step === 'email' && email) setStep('profile')
else if (step === 'profile' && name) setStep('password')
else if (step === 'password' && result?.valid) setStep('complete')
}
const back = () => {
if (step === 'profile') setStep('email')
else if (step === 'password') setStep('profile')
}
const stepNumber: Record<Exclude<Step, 'complete'>, number> = {
email: 1,
profile: 2,
password: 3,
}
return (
<div>
{step !== 'complete' && (
<h2>Create Account โ Step {stepNumber[step]} of 3</h2>
)}
{step === 'email' && (
<div>
<label>Email</label>
<input type="email" value={email} onChange={(e) => setEmail(e.target.value)} />
<button onClick={next} disabled={!email}>
Next
</button>
</div>
)}
{step === 'profile' && (
<div>
<label>Full Name</label>
<input type="text" value={name} onChange={(e) => setName(e.target.value)} />
<button onClick={back}>Back</button>
<button onClick={next} disabled={!name}>
Next
</button>
</div>
)}
{step === 'password' && (
<div>
<label>Password</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
{result?.feedback.suggestions.map((msg, i) => <p key={i}>{msg}</p>)}
<button onClick={back}>Back</button>
<button onClick={next} disabled={!result?.valid}>
Complete
</button>
</div>
)}
{step === 'complete' && (
<div>
<h3>Account Created!</h3>
<p>Email: {email}</p>
<p>Name: {name}</p>
</div>
)}
</div>
)
}More Examples โ
Browse the examples/ directory for runnable apps. Storybook stories for the React hook and PasswordInput component are runnable locally โ clone the repo, run pnpm install, then pnpm storybook (hook) or pnpm storybook:components (component).