A password field with a visibility toggle and a four-step strength guide that explains how to improve the entry.
Set a workspace password
The guide updates as you type.
Strength: Strong. Use 8+ characters, mixed case, a number, and a symbol.
import { PasswordInput } from "@/components/ballmac/password-input"
export default function PasswordInputDemo() {
return (
<div className="w-full max-w-sm rounded-xl border bg-card p-5 shadow-sm">
<p className="mb-1 text-sm font-semibold">Set a workspace password</p>
<p className="mb-4 text-xs text-muted-foreground">
The guide updates as you type.
</p>
<PasswordInput label="Workspace password" defaultValue="Orbit2026!" />
</div>
)
}Installation
$ pnpm dlx shadcn@latest add @ballmac/password-inputInstall the dependencies.
$ pnpm add lucide-reactAdd the Ballmac items it builds on.
$ pnpm dlx shadcn@latest add @ballmac/i18nCopy the source into your project.
components/ballmac/password-input.tsx// Ballmac UI: Password Input. https://ui.ballmac.com/components/password-input "use client" import * as React from "react" import { Eye, EyeOff } from "lucide-react" import { cn } from "@/lib/utils" import { useMessages } from "@/lib/ballmac/i18n" type PasswordInputProps = Omit< React.ComponentProps<"input">, "type" | "value" | "defaultValue" | "onChange" > & { /** Controlled password value. */ value?: string /** Initial password when uncontrolled. */ defaultValue?: string /** Called with the new password. */ onValueChange?: (value: string) => void /** Show a four-step strength guide. */ showStrength?: boolean /** Accessible name of the password field. */ label?: string } function PasswordInput({ value, defaultValue = "", onValueChange, showStrength = true, label, className, disabled, ...props }: PasswordInputProps) { const msg = useMessages() label ??= msg("password-input.label", "Password") const [internal, setInternal] = React.useState(defaultValue) const [visible, setVisible] = React.useState(false) const current = value ?? internal const score = Number(current.length >= 8) + Number(/[a-z]/.test(current) && /[A-Z]/.test(current)) + Number(/\d/.test(current)) + Number(/[^a-zA-Z\d]/.test(current)) const strength = [msg("password-input.tooShort", "Too short"), msg("password-input.needsWork", "Needs work"), msg("password-input.fair", "Fair"), msg("password-input.good", "Good"), msg("password-input.strong", "Strong")][score] function commit(next: string) { if (value === undefined) setInternal(next) onValueChange?.(next) } return ( <div data-slot="password-input" className={cn("w-full min-w-0", className)}> <div className="flex h-9 items-center rounded-md border border-input bg-background shadow-xs transition-[border-color,box-shadow] duration-150 motion-reduce:transition-none focus-within:border-ring focus-within:ring-[3px] focus-within:ring-ring/50"> <input data-slot="password-input-field" type={visible ? "text" : "password"} autoComplete="new-password" aria-label={label} disabled={disabled} value={current} onChange={(event) => commit(event.currentTarget.value)} className="h-full min-w-0 flex-1 bg-transparent px-3 text-sm text-foreground outline-none placeholder:text-muted-foreground disabled:opacity-50" {...props} /> <button type="button" aria-label={visible ? msg("password-input.hidePassword", "Hide password") : msg("password-input.showPassword", "Show password")} aria-pressed={visible} disabled={disabled} onClick={() => setVisible(!visible)} className="me-1 flex size-8 shrink-0 items-center justify-center rounded-md text-muted-foreground outline-none hover:bg-accent hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:opacity-50" > {visible ? ( <EyeOff aria-hidden="true" className="size-4" /> ) : ( <Eye aria-hidden="true" className="size-4" /> )} </button> </div> {showStrength && current.length > 0 && ( <div data-slot="password-strength" className="mt-2" aria-live="polite"> <div aria-hidden="true" className="grid grid-cols-4 gap-1"> {[1, 2, 3, 4].map((level) => ( <span key={level} className={cn( "h-1 rounded-full bg-muted", score >= level && (score < 3 ? "bg-destructive" : "bg-chart-2"), )} /> ))} </div> <p className="mt-1.5 text-xs text-muted-foreground"> {msg("password-input.strengthHint", "Strength: {strength}. Use 8+ characters, mixed case, a number, and a symbol.", { strength })} </p> </div> )} </div> ) } export { PasswordInput, type PasswordInputProps }Update the import paths to match your project setup.
Usage
import { PasswordInput } from "@/components/ballmac/password-input"The full example is in the Code tab above.
Examples
States and variants
Confirm your password
import { PasswordInput } from "@/components/ballmac/password-input"
export default function PasswordInputStates() {
return (
<div className="w-full max-w-sm">
<p className="mb-2 text-sm font-medium">Confirm your password</p>
<PasswordInput
label="Confirm password"
showStrength={false}
placeholder="Enter password"
/>
</div>
)
}API reference
| Prop | Type | Default |
|---|---|---|
valueControlled password value. | string | — |
defaultValueInitial password when uncontrolled. | string | "" |
onValueChangeCalled with the new password. | (value: string) => void | — |
showStrengthShow a four-step strength guide. | boolean | true |
labelAccessible name of the password field. | string | — |
Also accepts the standard attributes of its root element.
Accessibility
| Key | Action |
|---|---|
| Tab / Space | Toggles password visibility |
Use with AI
A password field with a visibility toggle and a four-step strength guide that explains how to improve the entry. With the shadcn MCP server set up (guide), ask your agent:
Add the Ballmac UI Password Input (@ballmac/password-input) to this project with the shadcn MCP, then use it where it fits.
Use it for
- Create or change a password
- Give immediate guidance while typing
Not for
- Use input for a standard text field
Registry JSON: https://ui.ballmac.com/r/password-input.json
Credits
Free to use in personal and commercial projects.
- npm
- lucide-react
- Registry
- @ballmac/i18nshadcn/utils
Pairs well with
Input
A text input in three heights that match Button, plus InputGroup and InputGroupAddon for leading or trailing icons, units and domains.
Calendar
A date grid for one day, several days or a range, with month and year selects, week numbers, disabled rules and range-end styling, on React DayPicker.
Color Picker
A native color chooser paired with an editable hex field and a live swatch.
Combobox
A searchable single-choice select with groups, descriptions, keywords, clearable value, invalid state and hidden-input form submission.