Password Strength
A password field with show/hide, a segmented strength meter and a checklist that ticks as you type.
$
View as Markdownnpx shadcn@latest add https://sundaeui.dev/r/password-strength.json'use client'
import { useId, useState } from 'react'
import './password-strength.css'
export interface PasswordRule {
label: string
test: (pw: string) => boolean
}
export const DEFAULT_RULES: PasswordRule[] = [
{ label: '8+ characters', test: (p) => p.length >= 8 },
{ label: 'A number', test: (p) => /\d/.test(p) },
{ label: 'Upper & lower case', test: (p) => /[a-z]/.test(p) && /[A-Z]/.test(p) },
{ label: 'A symbol', test: (p) => /[^\w\s]/.test(p) },
]
const WORDS = ['Too weak', 'Weak', 'Fair', 'Good', 'Strong']
const COLORS = ['#c2534b', '#8a8a8a', '#a8a8a8', '#cfcfcf', '#5f9e76']
export interface PasswordStrengthProps {
value?: string
onValueChange?: (v: string) => void
label?: string
rules?: PasswordRule[]
/** Meter segments. */
segments?: number
variant?: 'bars' | 'line'
fill?: 'dark' | 'light'
className?: string
}
/** A password field with a show/hide toggle, a segmented strength meter and a live checklist. */
export function PasswordStrength({ value, onValueChange, label = 'Password', rules = DEFAULT_RULES, segments = 4, variant = 'bars', fill = 'dark', className }: PasswordStrengthProps) {
const [inner, setInner] = useState('')
const [show, setShow] = useState(false)
const id = useId()
const pw = value ?? inner
const passed = rules.filter((r) => r.test(pw)).length
const score = pw ? Math.round((passed / rules.length) * (WORDS.length - 1)) : 0
const lit = pw ? Math.max(1, Math.round((passed / rules.length) * segments)) : 0
const color = COLORS[score]
return (
<div className={['sd-pw', className].filter(Boolean).join(' ')} data-fill={fill} data-variant={variant} style={{ '--pw-c': color } as React.CSSProperties}>
<label htmlFor={id} className="sd-pw__label">{label}</label>
<div className="sd-pw__field">
<input
id={id}
type={show ? 'text' : 'password'}
value={pw}
autoComplete="new-password"
aria-describedby={`${id}-s ${id}-r`}
onChange={(e) => {
if (value === undefined) setInner(e.target.value)
onValueChange?.(e.target.value)
}}
/>
<button type="button" onClick={() => setShow((s) => !s)} aria-pressed={show} aria-label={show ? 'Hide password' : 'Show password'} className="sd-pw__eye">
<svg viewBox="0 0 20 20" aria-hidden>
<path d="M2 10s3-5.5 8-5.5S18 10 18 10s-3 5.5-8 5.5S2 10 2 10Z" />
<circle cx="10" cy="10" r="2.5" />
{!show && <path d="m3.5 3.5 13 13" />}
</svg>
</button>
</div>
<div className="sd-pw__meter" aria-hidden>
{Array.from({ length: segments }, (_, i) => (
<i key={i} data-on={i < lit || undefined} style={{ '--i': i } as React.CSSProperties} />
))}
</div>
<p id={`${id}-s`} className="sd-pw__word" aria-live="polite">
{pw ? WORDS[score] : 'Enter a password'}
</p>
<ul id={`${id}-r`} className="sd-pw__rules">
{rules.map((r) => {
const ok = r.test(pw)
return (
<li key={r.label} data-ok={ok || undefined}>
<span aria-hidden>
<svg viewBox="0 0 12 12"><path d="m2.5 6.5 2 2 5-5" /></svg>
</span>
{r.label}
<span className="sd-sr">{ok ? ', met' : ', not met'}</span>
</li>
)
})}
</ul>
</div>
)
}.sd-pw { --fg: #fafafa; --mute: #8e8e8e; --bg: #1d1d1d; --field: #262626; --line: rgb(255 255 255 / 0.09); --off: rgb(255 255 255 / 0.1);
width: min(100%, 300px); padding: 16px; border-radius: 22px; background: var(--bg); color: var(--fg); box-shadow: inset 0 0 0 1px var(--line); }
.sd-pw[data-fill='light'] { --fg: #121212; --mute: #6f6f6f; --bg: #fff; --field: #f3f3f3; --line: rgb(0 0 0 / 0.08); --off: rgb(0 0 0 / 0.08); }
.sd-pw__label { font-size: 13px; color: var(--mute); }
.sd-pw__field { display: flex; align-items: center; margin-top: 6px; height: 46px; padding: 0 6px 0 14px; border-radius: 14px; background: var(--field); box-shadow: inset 0 0 0 1px var(--line); transition: box-shadow 200ms; }
.sd-pw__field:focus-within { box-shadow: inset 0 0 0 1.5px var(--pw-c); }
.sd-pw__field input { flex: 1; min-width: 0; background: none; border: 0; outline: none; color: inherit; font-size: 17px; letter-spacing: 0.04em; }
.sd-pw__eye { display: grid; place-items: center; width: 34px; height: 34px; border-radius: 10px; color: var(--mute); }
.sd-pw__eye svg { width: 19px; fill: none; stroke: currentColor; stroke-width: 1.5; stroke-linecap: round; }
.sd-pw button:focus-visible { outline: 2px solid var(--fg); outline-offset: 1px; }
.sd-pw__meter { display: flex; gap: 5px; margin-top: 12px; }
.sd-pw__meter i { flex: 1; height: 6px; border-radius: 6px; background: var(--off); transition: background-color 300ms; transition-delay: calc(var(--i) * 40ms); }
.sd-pw[data-variant='line'] .sd-pw__meter { gap: 0; }
.sd-pw[data-variant='line'] .sd-pw__meter i { border-radius: 0; height: 4px; }
.sd-pw[data-variant='line'] .sd-pw__meter i:first-child { border-radius: 4px 0 0 4px; }
.sd-pw[data-variant='line'] .sd-pw__meter i:last-child { border-radius: 0 4px 4px 0; }
.sd-pw__meter i[data-on] { background: var(--pw-c); }
.sd-pw__word { margin: 8px 0 10px; font-size: 14px; font-weight: 600; color: var(--pw-c); transition: color 300ms; }
.sd-pw__rules { display: grid; grid-template-columns: 1fr 1fr; gap: 6px 10px; margin: 0; padding: 0; list-style: none; font-size: 12.5px; color: var(--mute); }
.sd-pw__rules li { display: flex; align-items: center; gap: 6px; transition: color 250ms; }
.sd-pw__rules li > span:first-child { display: grid; place-items: center; width: 16px; height: 16px; border-radius: 99px; box-shadow: inset 0 0 0 1.5px var(--off); transition: background-color 250ms, box-shadow 250ms; }
.sd-pw__rules svg { width: 10px; fill: none; stroke: #0b0b0b; stroke-width: 2; stroke-linecap: round; opacity: 0; transform: scale(0.4); transition: opacity 200ms, transform 300ms cubic-bezier(0.3, 1.8, 0.4, 1); }
.sd-pw__rules li[data-ok] { color: var(--fg); }
.sd-pw__rules li[data-ok] > span:first-child { background: #22c55e; box-shadow: none; }
.sd-pw__rules li[data-ok] svg { opacity: 1; transform: none; }
.sd-sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
@media (prefers-reduced-motion: reduce) { .sd-pw * { transition: none !important; } }Overview
The meter fills and changes colour from red to green as rules are met; each rule ticks with a small pop.
Rules are functions you pass in, so it can match your server-side policy exactly.
When to use
- Sign-up, reset and change-password forms.
When not to use
- Login forms: do not grade passwords people already have.
Installation
$
pnpm dlx shadcn@latest add https://sundaeui.dev/r/password-strength.json$
npx shadcn@latest add https://sundaeui.dev/r/password-strength.json$
yarn dlx shadcn@latest add https://sundaeui.dev/r/password-strength.json$
bunx --bun shadcn@latest add https://sundaeui.dev/r/password-strength.jsonWorks in any React project. Files are written to components/sundae/. Read the source in the stage’s Code view.
- No dependencies beyond React 18 or later.
- Copy password-strength.tsx and password-strength.css into components/sundae/.
- The CSS file is imported by the component, so no extra configuration is needed.
- Import the component and use it as shown below.
Usage
import { PasswordStrength } from '@/components/sundae/password-strength'
<PasswordStrength value={pw} onValueChange={setPw} />Props
| Prop | Type | Default |
|---|---|---|
value / onValueChangeControlled value. | string / (v) => void | — |
rulesLabel and test per rule. | PasswordRule[] | 4 default rules |
segmentsMeter segments. | number | 4 |
variantMeter style. | 'bars' | 'line' | 'bars' |
fillSurface: dark or light. | 'dark' | 'light' | 'dark' |
Accessibility
- A labelled input with autocomplete="new-password", described by the strength word and the rule list.
- The strength word is a polite live region; each rule says "met" or "not met" to screen readers.
- The show/hide toggle is a pressed-state button.
Customization
- Every colour is a CSS custom property on the root class; override them in your own stylesheet.
- fill="light" switches the built-in light palette.
Keyboard
- Tab
- Field → show/hide toggle
FAQ
- Is Password Strength free?
- Yes. Password Strength is free for personal and commercial projects, with full source on this page.
- How do I install Password Strength?
- Run npx shadcn@latest add https://sundaeui.dev/r/password-strength.json. The files are written to components/sundae/. It has no dependencies beyond React 18 or later.
- Does Password Strength support keyboard and screen readers?
- A labelled input with autocomplete="new-password", described by the strength word and the rule list. The strength word is a polite live region; each rule says "met" or "not met" to screen readers.
- Does Password Strength respect reduced motion?
- Yes. With prefers-reduced-motion set, decorative motion is removed and state changes are instant.
More like this
All forms →Birthday Field
Forms · RefinedOTP Field
Forms · Refined- Pro
Onboarding Flow
Forms · Refined - Pro
Wheel Picker
Forms · Refined Phone Input
Forms · RefinedSign-in Card
Forms · Refined