Password Generator
Random passwords with a length slider, character toggles, strength meter and copy.
Pick at least one option—
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 />;
}
Edit with AIPro
Describe a change to Password Generator and get the updated code. The live preview above stays as the original.
Installation
Install the dependencies
$ npm i motion lucide-reactCopy the source into
components/ui/password-generator.tsxcomponents/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 />; }Use it
The file exports the component and a
Demoshowing typical usage. Import whichever you need.