import styles from './PasswordStrengthMeter.module.css' const PASSWORD_RULES = [ { id: 'length', test: (value: string) => value.length >= 8, }, { id: 'upper', test: (value: string) => /[A-Z]/.test(value), }, { id: 'number', test: (value: string) => /\d/.test(value), }, ] as const export type PasswordRuleId = (typeof PASSWORD_RULES)[number]['id'] | 'match' export type StrengthTone = 'empty' | 'weak' | 'fair' | 'good' | 'strong' export type PasswordStrengthLabels = { title: string empty: string weak: string fair: string good: string strong: string rules: Record } const DEFAULT_LABELS: PasswordStrengthLabels = { title: 'Password strength', empty: 'Enter a password', weak: 'Weak', fair: 'Fair', good: 'Good', strong: 'Strong', rules: { length: 'At least 8 characters', upper: 'One uppercase letter', number: 'One number', match: 'Passwords match', }, } function passwordsMatch(password: string, confirmPassword: string) { return confirmPassword.length > 0 && password === confirmPassword } export function getPasswordChecks( password: string, labels: PasswordStrengthLabels = DEFAULT_LABELS, confirmPassword = '', ) { return [ ...PASSWORD_RULES.map((rule) => ({ id: rule.id, label: labels.rules[rule.id], met: rule.test(password), })), { id: 'match' as const, label: labels.rules.match, met: passwordsMatch(password, confirmPassword), }, ] } export function getStrengthMeta( metCount: number, hasInput: boolean, labels: PasswordStrengthLabels = DEFAULT_LABELS, ): { tone: StrengthTone label: string percent: number } { if (!hasInput || metCount === 0) { return { tone: 'empty', label: labels.empty, percent: 0 } } if (metCount === 1) return { tone: 'weak', label: labels.weak, percent: 25 } if (metCount === 2) return { tone: 'fair', label: labels.fair, percent: 50 } if (metCount === 3) return { tone: 'good', label: labels.good, percent: 75 } return { tone: 'strong', label: labels.strong, percent: 100 } } export function isPasswordStrong(password: string): boolean { return PASSWORD_RULES.every((rule) => rule.test(password)) } export function getPasswordValidationError( password: string, labels: PasswordStrengthLabels = DEFAULT_LABELS, ): string | null { const unmet = PASSWORD_RULES.filter((rule) => !rule.test(password)) if (unmet.length === 0) return null return unmet.map((rule) => labels.rules[rule.id]).join(', ') } export interface PasswordStrengthMeterProps { password: string confirmPassword?: string labels?: PasswordStrengthLabels id?: string } export function PasswordStrengthMeter({ password, confirmPassword = '', labels = DEFAULT_LABELS, id = 'password-strength', }: PasswordStrengthMeterProps) { const checks = getPasswordChecks(password, labels, confirmPassword) const metCount = checks.filter((check) => check.met).length const strength = getStrengthMeta(metCount, password.length > 0, labels) return (
{labels.title} {strength.label}
    {checks.map((check) => (
  • ))}
) }