# Password Input

A show/hide eye whose slash draws on and off, and a strength meter that fills as you meet each rule.

**Category:** Inputs  
**Exports:** `PasswordInput`  
**npm dependencies:** motion (+ clsx, tailwind-merge for `cn`)

## Install

```bash
npx shadcn@latest add https://charcoalui.com/r/password-input.json
```
The file lands at `components/ui/password-input.tsx`; import from `@/components/ui/password-input`.

## API — Password Input
| Prop | Type | Default | Notes |
| --- | --- | --- | --- |
| `label` | `string` | `"Password"` |  |
| `showStrength` | `boolean` | `true` |  |
| `...props` | `ComponentProps<"input">` | — |  |

## Example — Password Input

```tsx
import { PasswordInput } from "@/components/ui/password-input";

export default function PasswordInputDemo({
  label = "Create a password",
  placeholder = "At least 8 characters",
  showStrength = true,
  disabled = false,
}: {
  label?: string;
  placeholder?: string;
  showStrength?: boolean;
  disabled?: boolean;
}) {
  return (
    <div className="w-full max-w-sm">
      <PasswordInput label={label} placeholder={placeholder} showStrength={showStrength} disabled={disabled} />
    </div>
  );
}
```

## Source — components/ui/password-input.tsx

```tsx
"use client";

import * as React from "react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import { cn } from "@/lib/utils";

export type PasswordInputProps = Omit<React.ComponentProps<"input">, "type"> & {
  label?: string;
  /** Show the strength meter and checklist. */
  showStrength?: boolean;
};

const rules = [
  { label: "8 or more characters", test: (v: string) => v.length >= 8 },
  { label: "Upper and lower case", test: (v: string) => /[a-z]/.test(v) && /[A-Z]/.test(v) },
  { label: "A number", test: (v: string) => /\d/.test(v) },
  { label: "A symbol", test: (v: string) => /[^A-Za-z0-9]/.test(v) },
];
const levels = [
  { label: "Too weak", bar: "bg-destructive" },
  { label: "Weak", bar: "bg-destructive" },
  { label: "Fair", bar: "bg-ember" },
  { label: "Good", bar: "bg-foreground" },
  { label: "Strong", bar: "bg-success" },
];

export function PasswordInput({ label = "Password", showStrength = true, className, id: idProp, value, defaultValue, onChange, ...props }: PasswordInputProps) {
  const autoId = React.useId();
  const id = idProp ?? autoId;
  const reduce = useReducedMotion();
  const [visible, setVisible] = React.useState(false);
  const [inner, setInner] = React.useState(String(defaultValue ?? ""));
  const text = value !== undefined ? String(value) : inner;
  const score = rules.filter((r) => r.test(text)).length;

  return (
    <div className={cn("grid w-full gap-1.5", className)}>
      <label htmlFor={id} className="text-sm font-medium">
        {label}
      </label>
      <div className="relative flex h-11 items-center rounded-lg border border-input bg-background transition-[border-color,box-shadow] focus-within:border-foreground/40 focus-within:ring-[3px] focus-within:ring-ring/25">
        <input
          id={id}
          type={visible ? "text" : "password"}
          autoComplete="new-password"
          aria-describedby={showStrength ? `${id}-strength` : undefined}
          value={value}
          defaultValue={value === undefined ? defaultValue : undefined}
          onChange={(e) => {
            setInner(e.target.value);
            onChange?.(e);
          }}
          className="h-full min-w-0 flex-1 bg-transparent px-3.5 text-base outline-none placeholder:text-muted-foreground sm:text-sm"
          {...props}
        />
        <button
          type="button"
          onClick={() => setVisible((v) => !v)}
          aria-label={visible ? "Hide password" : "Show password"}
          aria-pressed={visible}
          className="mr-1 grid size-9 cursor-pointer place-items-center rounded-md text-muted-foreground outline-none transition-colors hover:bg-accent hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring"
        >
          <svg viewBox="0 0 24 24" className="size-[18px]" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
            <path d="M2.5 12S6 5.5 12 5.5 21.5 12 21.5 12 18 18.5 12 18.5 2.5 12 2.5 12Z" />
            <motion.circle cx="12" cy="12" r="3" animate={{ scale: visible ? 1 : 0.6 }} transition={{ type: "spring", stiffness: 500, damping: 20 }} />
            <motion.path
              d="M4 4l16 16"
              initial={false}
              animate={{ pathLength: visible ? 0 : 1, opacity: visible ? 0 : 1 }}
              transition={{ duration: reduce ? 0 : 0.22 }}
            />
          </svg>
        </button>
      </div>
      {showStrength && (
        <div id={`${id}-strength`} className="grid gap-2 pt-1">
          <div className="flex items-center gap-3">
            <div className="grid flex-1 grid-cols-4 gap-1.5" aria-hidden>
              {[1, 2, 3, 4].map((n) => (
                <span key={n} className="h-1.5 overflow-hidden rounded-full bg-muted">
                  <motion.span
                    className={cn("block h-full rounded-full", levels[score].bar)}
                    initial={false}
                    animate={{ scaleX: n <= score ? 1 : 0 }}
                    style={{ originX: 0 }}
                    transition={reduce ? { duration: 0 } : { type: "spring", stiffness: 300, damping: 26, delay: (n - 1) * 0.04 }}
                  />
                </span>
              ))}
            </div>
            <span className="w-16 text-right text-xs text-muted-foreground" aria-live="polite">
              {text ? levels[score].label : ""}
            </span>
          </div>
          <ul className="grid grid-cols-2 gap-x-3 gap-y-1 text-[13px]">
            {rules.map((r) => {
              const ok = r.test(text);
              return (
                <li key={r.label} className={cn("flex items-center gap-1.5 transition-colors", ok ? "text-foreground" : "text-muted-foreground")}>
                  <span className="relative grid size-3.5 place-items-center">
                    <AnimatePresence initial={false} mode="popLayout">
                      {ok ? (
                        <motion.svg key="ok" viewBox="0 0 24 24" className="size-3.5 text-success" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round" initial={{ scale: 0 }} animate={{ scale: 1 }} exit={{ scale: 0 }}>
                          <path d="M5 12.5l4.5 4.5L19 7" />
                        </motion.svg>
                      ) : (
                        <motion.span key="dot" className="size-1.5 rounded-full bg-muted-foreground/50" initial={{ scale: 0 }} animate={{ scale: 1 }} exit={{ scale: 0 }} />
                      )}
                    </AnimatePresence>
                  </span>
                  {r.label}
                  <span className="sr-only">{ok ? "(met)" : "(not met)"}</span>
                </li>
              );
            })}
          </ul>
        </div>
      )}
    </div>
  );
}
```