Web InnoventixFreeCode

Password Toggle Input

Original · free

password field with show/hide toggle

byWeb InnoventixReact + Tailwind
inppasswordtoggleinputs
Open

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
inp-password-toggle.tsx
"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&rsquo;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

motion

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 quote

Similar components

Browse all →