Password Toggle Input
Original · freepassword field with show/hide toggle
Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.
npx shadcn@latest add https://webinnoventix.com/r/inp-password-toggle.json"use client";
import { useState, useMemo } from "react";
import { motion, AnimatePresence, useReducedMotion } from "motion/react";
/* ---------------------------------------------------------------- icons */
function EyeIcon({ className }: { className?: string }) {
return (
<svg
className={className}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={1.75}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<path d="M2.5 12S5.5 5.5 12 5.5 21.5 12 21.5 12 18.5 18.5 12 18.5 2.5 12 2.5 12Z" />
<circle cx="12" cy="12" r="3" />
</svg>
);
}
function EyeOffIcon({ className }: { className?: string }) {
return (
<svg
className={className}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={1.75}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<path d="M4 4 20 20" />
<path d="M9.9 5.2A9.6 9.6 0 0 1 12 5c6.5 0 9.5 6.5 9.5 6.5a15.4 15.4 0 0 1-2.8 3.7" />
<path d="M6.3 7.1A15.6 15.6 0 0 0 2.5 11.5S5.5 18 12 18a9.2 9.2 0 0 0 3.3-.6" />
<path d="M9.9 9.9a3 3 0 0 0 4.2 4.2" />
</svg>
);
}
function CheckIcon({ className }: { className?: string }) {
return (
<svg
className={className}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2.4}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<path d="M20 6 9 17l-5-5" />
</svg>
);
}
function DotIcon({ className }: { className?: string }) {
return (
<svg className={className} viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<circle cx="12" cy="12" r="3" />
</svg>
);
}
function AlertIcon({ className }: { className?: string }) {
return (
<svg
className={className}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={1.9}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<path d="M12 3 2.5 20h19L12 3Z" />
<path d="M12 10v4" />
<path d="M12 17.5h.01" />
</svg>
);
}
function LockIcon({ className }: { className?: string }) {
return (
<svg
className={className}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={1.75}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<rect x="4.5" y="10.5" width="15" height="10" rx="2.2" />
<path d="M8 10.5V7.5a4 4 0 0 1 8 0v3" />
</svg>
);
}
/* ------------------------------------------------------------- strength */
interface Requirement {
id: string;
label: string;
met: boolean;
}
interface Strength {
score: number; // 0..4
label: string;
requirements: Requirement[];
}
function evaluate(pw: string): Strength {
const requirements: Requirement[] = [
{ id: "len", label: "At least 8 characters", met: pw.length >= 8 },
{ id: "upper", label: "One uppercase letter", met: /[A-Z]/.test(pw) },
{ id: "lower", label: "One lowercase letter", met: /[a-z]/.test(pw) },
{ id: "num", label: "One number", met: /\d/.test(pw) },
{ id: "sym", label: "One symbol (!@#$…)", met: /[^A-Za-z0-9]/.test(pw) },
];
const met = requirements.filter((r) => r.met).length;
let score = 0;
if (pw.length > 0) {
if (met <= 2) score = 1;
else if (met === 3) score = 2;
else if (met === 4) score = 3;
else score = 4;
}
const labels = ["Enter a password", "Weak", "Fair", "Good", "Strong"];
return { score, label: labels[score], requirements };
}
const BAR_FILL = ["bg-rose-500", "bg-amber-500", "bg-sky-500", "bg-emerald-500"];
const LABEL_TONE = [
"text-slate-400 dark:text-slate-500",
"text-rose-600 dark:text-rose-400",
"text-amber-600 dark:text-amber-400",
"text-sky-600 dark:text-sky-400",
"text-emerald-600 dark:text-emerald-400",
];
/* --------------------------------------------------------- field widget */
interface PasswordFieldProps {
id: string;
label: string;
value: string;
onChange: (value: string) => void;
placeholder?: string;
autoComplete?: string;
describedBy?: string;
invalid?: boolean;
hint?: string;
}
function PasswordField(props: PasswordFieldProps) {
const { id, label, value, onChange, placeholder, autoComplete, describedBy, invalid, hint } =
props;
const [visible, setVisible] = useState(false);
const [caps, setCaps] = useState(false);
const reduce = useReducedMotion();
const capsId = `${id}-caps`;
const described =
[describedBy, caps ? capsId : null].filter(Boolean).join(" ") || undefined;
return (
<div>
<div className="mb-1.5 flex items-baseline justify-between gap-3">
<label
htmlFor={id}
className="text-sm font-medium text-slate-700 dark:text-slate-200"
>
{label}
</label>
{hint ? (
<span className="text-xs text-slate-400 dark:text-slate-500">{hint}</span>
) : null}
</div>
<div className="relative">
<input
id={id}
type={visible ? "text" : "password"}
value={value}
onChange={(e) => onChange(e.target.value)}
onKeyUp={(e) => setCaps(e.getModifierState("CapsLock"))}
onKeyDown={(e) => setCaps(e.getModifierState("CapsLock"))}
onBlur={() => setCaps(false)}
placeholder={placeholder}
autoComplete={autoComplete}
spellCheck={false}
aria-invalid={invalid || undefined}
aria-describedby={described}
className={`w-full rounded-xl border bg-white px-3.5 py-2.5 pr-12 text-[15px] text-slate-900 placeholder:text-slate-400 shadow-sm outline-none transition-colors focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:bg-slate-950 dark:text-slate-50 dark:placeholder:text-slate-500 dark:focus-visible:ring-offset-slate-900 ${
invalid
? "border-rose-400 focus-visible:ring-rose-500 dark:border-rose-500/70"
: "border-slate-300 focus-visible:ring-indigo-500 dark:border-slate-700"
}`}
/>
<button
type="button"
onClick={() => setVisible((v) => !v)}
aria-pressed={visible}
aria-controls={id}
aria-label={visible ? "Hide password" : "Show password"}
title={visible ? "Hide password" : "Show password"}
className="absolute right-1.5 top-1/2 grid h-9 w-9 -translate-y-1/2 place-items-center rounded-lg text-slate-500 transition-colors hover:bg-slate-100 hover:text-slate-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-slate-100"
>
<span className="relative block h-5 w-5">
<AnimatePresence initial={false} mode="wait">
<motion.span
key={visible ? "on" : "off"}
className="absolute inset-0"
initial={reduce ? false : { opacity: 0, rotate: -35, scale: 0.7 }}
animate={{ opacity: 1, rotate: 0, scale: 1 }}
exit={reduce ? { opacity: 0 } : { opacity: 0, rotate: 35, scale: 0.7 }}
transition={{ duration: reduce ? 0 : 0.16, ease: "easeOut" }}
>
{visible ? (
<EyeOffIcon className="h-5 w-5" />
) : (
<EyeIcon className="h-5 w-5" />
)}
</motion.span>
</AnimatePresence>
</span>
</button>
</div>
<AnimatePresence initial={false}>
{caps ? (
<motion.p
id={capsId}
role="alert"
className="mt-1.5 flex items-center gap-1.5 text-xs font-medium text-amber-600 dark:text-amber-400"
initial={reduce ? { opacity: 0 } : { opacity: 0, height: 0, y: -4 }}
animate={reduce ? { opacity: 1 } : { opacity: 1, height: "auto", y: 0 }}
exit={reduce ? { opacity: 0 } : { opacity: 0, height: 0, y: -4 }}
transition={{ duration: reduce ? 0 : 0.18 }}
>
<AlertIcon className="h-3.5 w-3.5 shrink-0 [animation:iptg-pop_.25s_ease-out]" />
Caps Lock is on
</motion.p>
) : null}
</AnimatePresence>
</div>
);
}
/* --------------------------------------------------------------- shells */
function Card({ children }: { children: React.ReactNode }) {
return (
<div className="rounded-2xl border border-slate-200 bg-white p-6 shadow-sm shadow-slate-200/50 dark:border-slate-800 dark:bg-slate-900 dark:shadow-none">
{children}
</div>
);
}
function CardHead({ title, sub }: { title: string; sub: string }) {
return (
<div className="mb-5 flex items-start gap-3">
<span className="mt-0.5 grid h-9 w-9 shrink-0 place-items-center rounded-xl bg-indigo-50 text-indigo-600 ring-1 ring-inset ring-indigo-100 dark:bg-indigo-500/10 dark:text-indigo-300 dark:ring-indigo-500/20">
<LockIcon className="h-5 w-5" />
</span>
<div>
<h3 className="text-base font-semibold text-slate-900 dark:text-white">{title}</h3>
<p className="mt-0.5 text-sm text-slate-500 dark:text-slate-400">{sub}</p>
</div>
</div>
);
}
function SubmitButton({ children }: { children: React.ReactNode }) {
return (
<button
type="submit"
className="w-full rounded-xl bg-indigo-600 px-4 py-2.5 text-sm font-semibold text-white shadow-sm transition-colors hover:bg-indigo-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white active:bg-indigo-700 dark:focus-visible:ring-offset-slate-900"
>
{children}
</button>
);
}
/* ------------------------------------------------------------- showcase */
export default function InpPasswordToggle() {
const reduce = useReducedMotion();
// Variant A — sign in
const [signinPw, setSigninPw] = useState("");
// Variant B — create with strength
const [createPw, setCreatePw] = useState("");
const strength = useMemo(() => evaluate(createPw), [createPw]);
// Variant C — confirm match
const [newPw, setNewPw] = useState("");
const [confirmPw, setConfirmPw] = useState("");
const mismatch = confirmPw.length > 0 && confirmPw !== newPw;
const matched = confirmPw.length > 0 && confirmPw === newPw;
return (
<section className="relative w-full bg-slate-50 px-4 py-20 text-slate-900 dark:bg-slate-950 dark:text-slate-100 sm:px-6 lg:px-8">
<style>{`
@keyframes iptg-pop {
0% { transform: scale(.55); opacity: 0; }
60% { transform: scale(1.18); opacity: 1; }
100% { transform: scale(1); opacity: 1; }
}
@keyframes iptg-shimmer {
0% { background-position: -180% 0; }
100% { background-position: 180% 0; }
}
@media (prefers-reduced-motion: reduce) {
.iptg-shimmer, [class*="iptg-pop"] { animation: none !important; }
}
`}</style>
<div className="mx-auto max-w-6xl">
<header className="mx-auto max-w-2xl text-center">
<span className="inline-flex items-center gap-1.5 rounded-full border border-slate-200 bg-white px-3 py-1 text-xs font-medium text-slate-500 dark:border-slate-800 dark:bg-slate-900 dark:text-slate-400">
<LockIcon className="h-3.5 w-3.5" />
Inputs / Password
</span>
<h2 className="mt-4 text-3xl font-bold tracking-tight text-slate-900 dark:text-white sm:text-4xl">
Password field, done right
</h2>
<p className="mt-3 text-base text-slate-600 dark:text-slate-400">
Show / hide toggle, live Caps Lock detection, a strength meter and match
validation — all keyboard accessible with visible focus rings.
</p>
</header>
<div className="mt-12 grid gap-6 lg:grid-cols-3">
{/* Variant A ---------------------------------------------------- */}
<Card>
<CardHead title="Sign in" sub="Reveal to double-check before you submit." />
<form className="space-y-4" onSubmit={(e) => e.preventDefault()}>
<div>
<label
htmlFor="a-email"
className="mb-1.5 block text-sm font-medium text-slate-700 dark:text-slate-200"
>
Email
</label>
<input
id="a-email"
type="email"
autoComplete="email"
placeholder="ada@lovelace.io"
className="w-full rounded-xl border border-slate-300 bg-white px-3.5 py-2.5 text-[15px] text-slate-900 placeholder:text-slate-400 shadow-sm outline-none transition-colors focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:border-slate-700 dark:bg-slate-950 dark:text-slate-50 dark:placeholder:text-slate-500 dark:focus-visible:ring-offset-slate-900"
/>
</div>
<PasswordField
id="a-pw"
label="Password"
value={signinPw}
onChange={setSigninPw}
placeholder="Your password"
autoComplete="current-password"
hint="Forgot?"
/>
<SubmitButton>Sign in</SubmitButton>
</form>
</Card>
{/* Variant B ---------------------------------------------------- */}
<Card>
<CardHead
title="Create password"
sub="Live strength meter and requirement checklist."
/>
<form className="space-y-4" onSubmit={(e) => e.preventDefault()}>
<PasswordField
id="b-pw"
label="New password"
value={createPw}
onChange={setCreatePw}
placeholder="Make it strong"
autoComplete="new-password"
describedBy="b-meter b-reqs"
/>
<div id="b-meter">
<div className="flex gap-1.5" aria-hidden="true">
{[0, 1, 2, 3].map((i) => {
const filled = i < strength.score;
return (
<span
key={i}
className="relative h-1.5 flex-1 overflow-hidden rounded-full bg-slate-200 dark:bg-slate-800"
>
<motion.span
className={`block h-full origin-left rounded-full ${BAR_FILL[Math.max(0, strength.score - 1)]}`}
initial={false}
animate={{ scaleX: filled ? 1 : 0 }}
transition={{ duration: reduce ? 0 : 0.35, ease: "easeOut" }}
/>
{filled ? (
<span
className="iptg-shimmer pointer-events-none absolute inset-0 [animation:iptg-shimmer_1.8s_linear_infinite]"
style={{
backgroundImage:
"linear-gradient(90deg, transparent 0%, rgba(255,255,255,.55) 50%, transparent 100%)",
backgroundSize: "180% 100%",
}}
/>
) : null}
</span>
);
})}
</div>
<p
className={`mt-1.5 text-xs font-medium ${LABEL_TONE[strength.score]}`}
aria-live="polite"
>
{createPw.length === 0
? strength.label
: `Strength: ${strength.label}`}
</p>
</div>
<ul id="b-reqs" className="grid grid-cols-1 gap-1.5 sm:grid-cols-2">
{strength.requirements.map((r) => (
<li
key={r.id}
className={`flex items-center gap-1.5 text-xs transition-colors ${
r.met
? "text-emerald-600 dark:text-emerald-400"
: "text-slate-400 dark:text-slate-500"
}`}
>
{r.met ? (
<CheckIcon className="h-3.5 w-3.5 shrink-0 [animation:iptg-pop_.25s_ease-out]" />
) : (
<DotIcon className="h-3.5 w-3.5 shrink-0 opacity-60" />
)}
<span>{r.label}</span>
</li>
))}
</ul>
<SubmitButton>Create account</SubmitButton>
</form>
</Card>
{/* Variant C ---------------------------------------------------- */}
<Card>
<CardHead title="Confirm password" sub="Both fields must match to continue." />
<form className="space-y-4" onSubmit={(e) => e.preventDefault()}>
<PasswordField
id="c-pw"
label="New password"
value={newPw}
onChange={setNewPw}
placeholder="Choose a password"
autoComplete="new-password"
/>
<PasswordField
id="c-confirm"
label="Confirm password"
value={confirmPw}
onChange={setConfirmPw}
placeholder="Repeat it"
autoComplete="new-password"
invalid={mismatch}
describedBy="c-status"
/>
<div id="c-status" aria-live="polite" className="min-h-[1.25rem]">
<AnimatePresence mode="wait" initial={false}>
{mismatch ? (
<motion.p
key="mismatch"
className="flex items-center gap-1.5 text-xs font-medium text-rose-600 dark:text-rose-400"
initial={reduce ? { opacity: 0 } : { opacity: 0, y: -3 }}
animate={{ opacity: 1, y: 0 }}
exit={reduce ? { opacity: 0 } : { opacity: 0, y: -3 }}
transition={{ duration: reduce ? 0 : 0.15 }}
>
<AlertIcon className="h-3.5 w-3.5 shrink-0" />
Passwords don’t match yet
</motion.p>
) : matched ? (
<motion.p
key="match"
className="flex items-center gap-1.5 text-xs font-medium text-emerald-600 dark:text-emerald-400"
initial={reduce ? { opacity: 0 } : { opacity: 0, y: -3 }}
animate={{ opacity: 1, y: 0 }}
exit={reduce ? { opacity: 0 } : { opacity: 0, y: -3 }}
transition={{ duration: reduce ? 0 : 0.15 }}
>
<CheckIcon className="h-3.5 w-3.5 shrink-0 [animation:iptg-pop_.25s_ease-out]" />
Passwords match
</motion.p>
) : null}
</AnimatePresence>
</div>
<SubmitButton>Update password</SubmitButton>
</form>
</Card>
</div>
</div>
</section>
);
}Dependencies
Licence
Built by Web Innoventix. Free for personal and commercial use, no attribution required.
Built by Web Innoventix
Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.
Get a free quoteSimilar components
Browse all →
Basic Set Input
Originaldefault, hover, focus, disabled, error text inputs

Floating Label Input
Originallabel that floats up on focus/fill

Filled Input
Originalfilled material-style inputs

Underline Input
Originalminimal underline inputs with animated bar

Search Input
Originalsearch box with icon and clear

Search Suggest Input
Originalsearch with a live suggestions dropdown

Textarea Autosize Input
Originaltextarea that grows with content

Addon Group Input
Originalinput group with leading/trailing addons

Currency Input
Originalcurrency input with formatting

Tags Input
Originaltag input, add and remove chips

OTP Input
Original6-box one-time-code input with paste

Phone Input
Originalphone input with country prefix select

