Password input
A password field with a show and hide toggle and an optional strength meter.
npx shadcn@latest add https://ui.goodtabs.co/r/password-input.json
With the @goodtabs namespace in components.json: npx shadcn@latest add @goodtabs/password-input
Hidden by default. The eye button reveals the value and reports aria-pressed.
With a strength meter
Good
At least 8 characters with a mix of case, a number and a symbol.
Four bars and a label that update as you type. Monochrome, no colour.
In a sign in form
Usage
import { PasswordInput } from "@/components/ui/password-input"
<PasswordInput id="password" autoComplete="current-password" />
<PasswordInput showStrength autoComplete="new-password" />Props
| Prop | Type | Default | Description |
|---|---|---|---|
| showStrength | boolean | false | Show a four-bar strength meter and a Weak to Strong label under the field. |
| value, defaultValue, onChange | input props | Controlled or uncontrolled, exactly as on a plain input. |
Source
password-input/password-input.tsx
"use client";
import * as React from "react";
import { EyeIcon, EyeOffIcon } from "lucide-react";
import { Input } from "@/components/ui/input";
import { cn } from "@/lib/utils";
type PasswordInputProps = Omit<React.ComponentProps<typeof Input>, "type"> & {
/** Show a four-bar strength meter and a label under the field. */
showStrength?: boolean;
};
const STRENGTH_LABELS = ["Weak", "Weak", "Fair", "Good", "Strong"] as const;
/** A simple score from 0 to 4: length, mixed case, a number, a symbol. */
export function passwordStrength(value: string): number {
if (!value) return 0;
let score = 0;
if (value.length >= 8) score += 1;
if (/[a-z]/.test(value) && /[A-Z]/.test(value)) score += 1;
if (/\d/.test(value)) score += 1;
if (/[^a-zA-Z0-9]/.test(value)) score += 1;
return score;
}
/**
* A password field with a show and hide toggle, and an optional strength meter.
* Works controlled or uncontrolled like a plain input.
*/
export function PasswordInput({
showStrength = false,
value,
defaultValue,
onChange,
className,
disabled,
...props
}: PasswordInputProps) {
const [visible, setVisible] = React.useState(false);
const [inner, setInner] = React.useState(defaultValue === undefined ? "" : String(defaultValue));
const current = value === undefined ? inner : String(value);
const meterId = React.useId();
const score = passwordStrength(current);
const label = current ? STRENGTH_LABELS[score] : "";
return (
<div data-slot="password-input" className={cn("flex w-full flex-col gap-2", className)}>
<div className="relative flex items-center">
<Input
{...props}
type={visible ? "text" : "password"}
value={value}
defaultValue={defaultValue}
onChange={(e) => {
if (value === undefined) setInner(e.target.value);
onChange?.(e);
}}
disabled={disabled}
aria-describedby={showStrength ? cn(meterId, props["aria-describedby"]) : props["aria-describedby"]}
className="pr-9"
/>
<button
type="button"
onClick={() => setVisible((v) => !v)}
disabled={disabled}
aria-label={visible ? "Hide password" : "Show password"}
aria-pressed={visible}
className="absolute right-1.5 inline-grid size-6 place-items-center rounded-sm text-muted-foreground transition-colors duration-100 ease-out outline-none hover:bg-muted hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50"
>
{visible ? (
<EyeOffIcon className="size-4" aria-hidden="true" />
) : (
<EyeIcon className="size-4" aria-hidden="true" />
)}
</button>
</div>
{showStrength && (
<div id={meterId} className="flex items-center gap-2">
<div className="flex flex-1 gap-1" aria-hidden="true">
{[1, 2, 3, 4].map((step) => (
<span
key={step}
data-filled={score >= step || undefined}
className="h-1 flex-1 rounded-full bg-muted transition-colors duration-150 ease-out data-filled:bg-foreground"
/>
))}
</div>
<span
role="status"
aria-live="polite"
className="min-w-12 text-right font-mono text-xs text-muted-foreground tabular-nums"
>
{label}
</span>
</div>
)}
</div>
);
}
For AI agents
Hand your agent this page, the markdown at /components/password-input/prompt.md, or the Copy AI prompt button above.
When to use
- Sign in, sign up and change password forms
- Any secret the person types and may want to check, such as an API key
- Set showStrength on new password fields only
When not to use
- One-time codes: use the pin input
- As a real password policy; the score is a hint, enforce rules on the server
- For values that must never be revealed on screen
Agent notes
- Client component. It extends shadcn's Input, so id, autoComplete, disabled and every input prop pass through
- className goes on the wrapper, so set widths there; the field is always full width inside it
- The strength score is 0 to 4: 8 or more characters, mixed case, a number, a symbol. passwordStrength is exported if you need it elsewhere
- Pair with a Label via id and htmlFor; the meter is linked with aria-describedby and the label is a polite live region