Password Generator

Random passwords with a length slider, character toggles, strength meter and copy.

Pick at least one option
—

Edit with AIPro

Describe a change to Password Generator and get the updated code. The live preview above stays as the original.

Installation

  1. Install the dependencies

    $ npm i motion lucide-react
  2. Copy the source into components/ui/password-generator.tsx

    components/ui/password-generator.tsx
    "use client";
    
    import { motion } from "motion/react";
    import { Check, Copy, RefreshCw } from "lucide-react";
    import { useCallback, useEffect, useState } from "react";
    
    const SETS = {
      lower: "abcdefghijkmnopqrstuvwxyz",
      upper: "ABCDEFGHJKLMNPQRSTUVWXYZ",
      digits: "23456789",
      symbols: "!@#$%^&*-_=+?",
    } as const;
    type SetKey = keyof typeof SETS;
    
    /** Cryptographically random password using only the chosen character sets (look-alike characters are left out). */
    export function generatePassword(length: number, use: Record<SetKey, boolean>) {
      const pools = (Object.keys(SETS) as SetKey[]).filter((k) => use[k]).map((k) => SETS[k]);
      if (!pools.length) return "";
      const all = pools.join("");
      const random = new Uint32Array(length);
      crypto.getRandomValues(random);
      const chars = Array.from(random, (n) => all[n % all.length]);
      // Guarantee one character from every chosen set.
      pools.forEach((pool, i) => {
        if (i < length) chars[i] = pool[random[i] % pool.length];
      });
      // Shuffle so the guaranteed characters aren't always at the start.
      for (let i = chars.length - 1; i > 0; i--) {
        const j = random[i] % (i + 1);
        [chars[i], chars[j]] = [chars[j], chars[i]];
      }
      return chars.join("");
    }
    
    const LABELS: Record<SetKey, string> = { lower: "a–z", upper: "A–Z", digits: "0–9", symbols: "!@#" };
    
    /** Password generator with a length slider, character toggles, a strength meter and copy. */
    export function PasswordGenerator() {
      const [length, setLength] = useState(18);
      const [use, setUse] = useState<Record<SetKey, boolean>>({ lower: true, upper: true, digits: true, symbols: false });
      const [password, setPassword] = useState("");
      const [copied, setCopied] = useState(false);
    
      // Generated after mount so the static page and the browser never disagree.
      const regenerate = useCallback(() => setPassword(generatePassword(length, use)), [length, use]);
      useEffect(regenerate, [regenerate]);
    
      const poolSize = (Object.keys(SETS) as SetKey[]).reduce((n, k) => n + (use[k] ? SETS[k].length : 0), 0);
      const bits = poolSize ? Math.round(length * Math.log2(poolSize)) : 0;
      const level = bits >= 100 ? 3 : bits >= 70 ? 2 : bits >= 45 ? 1 : 0;
      const strength = [
        { label: "Weak", color: "#e11d48" },
        { label: "Okay", color: "#f59e0b" },
        { label: "Strong", color: "#16a34a" },
        { label: "Very strong", color: "#059669" },
      ][level];
    
      async function copy() {
        try {
          await navigator.clipboard.writeText(password);
          setCopied(true);
          setTimeout(() => setCopied(false), 1500);
        } catch {}
      }
    
      return (
        <div className="w-full max-w-sm rounded-2xl border border-zinc-200 bg-white p-5 shadow-sm">
          <div className="flex items-center gap-2 rounded-xl bg-zinc-950 p-3">
            <motion.code key={password} initial={{ opacity: 0, y: 4 }} animate={{ opacity: 1, y: 0 }} className="min-w-0 flex-1 truncate font-mono text-sm text-emerald-300" aria-live="polite">
              {password || "Pick at least one option"}
            </motion.code>
            <button aria-label="New password" onClick={regenerate} className="rounded-lg p-1.5 text-zinc-400 hover:bg-white/10 hover:text-white">
              <RefreshCw className="size-4" />
            </button>
            <button aria-label="Copy password" onClick={copy} disabled={!password} className="rounded-lg p-1.5 text-zinc-400 hover:bg-white/10 hover:text-white disabled:opacity-40">
              {copied ? <Check className="size-4 text-emerald-400" /> : <Copy className="size-4" />}
            </button>
          </div>
    
          <div className="mt-3 flex items-center gap-2">
            <div className="flex flex-1 gap-1">
              {[0, 1, 2, 3].map((i) => (
                <motion.span key={i} className="h-1.5 flex-1 rounded-full" animate={{ backgroundColor: i <= level && password ? strength.color : "#e4e4e7" }} />
              ))}
            </div>
            <span className="w-24 text-right text-xs font-medium" style={{ color: password ? strength.color : "#a1a1aa" }}>
              {password ? `${strength.label} · ${bits} bits` : "—"}
            </span>
          </div>
    
          <label className="mt-5 flex items-center justify-between text-sm text-zinc-700">
            Length <span className="font-semibold tabular-nums text-zinc-900">{length}</span>
          </label>
          <input type="range" min={8} max={40} value={length} onChange={(e) => setLength(Number(e.target.value))} aria-label="Password length" className="mt-2 h-1.5 w-full cursor-pointer appearance-none rounded-full bg-zinc-200 accent-zinc-900" />
    
          <div className="mt-4 grid grid-cols-4 gap-2">
            {(Object.keys(SETS) as SetKey[]).map((k) => (
              <button key={k} role="switch" aria-checked={use[k]} onClick={() => setUse((u) => ({ ...u, [k]: !u[k] }))} className={`rounded-lg border py-2 font-mono text-xs transition ${use[k] ? "border-zinc-900 bg-zinc-900 text-white" : "border-zinc-200 text-zinc-500 hover:border-zinc-300"}`}>
                {LABELS[k]}
              </button>
            ))}
          </div>
        </div>
      );
    }
    
    export default function Demo() {
      return <PasswordGenerator />;
    }
    
  3. Use it

    The file exports the component and a Demo showing typical usage. Import whichever you need.