Inputs

Password Generator

A crypto-backed password generator with a length slider, required character sets, look-alike exclusion, a live entropy readout and an explicit refusal for impossible rule sets.

Preview in your theme

Loading preview…

"use client"

import * as React from "react"
import { Check, Copy, CopyX, RefreshCw, TriangleAlert } from "lucide-react"
import { cn } from "@/lib/utils"

/** The four alphabets this generator can draw from. */
export type PasswordCharClass = "lowercase" | "uppercase" | "digits" | "symbols"

/** What the consumer learns about a password without having to re-derive it. */
export interface PasswordMeta {
  length: number
  /** Size of the de-duplicated union alphabet the characters were drawn from. */
  poolSize: number

Installation

npx shadcn@latest add https://ui.zyeon.ai/r/password-generator.json

Prompt

The prompt behind this component — paste it into your AI assistant to recreate or adapt it.

Build a React + TypeScript + Tailwind "PasswordGenerator" component
(lucide-react Check / Copy / CopyX / RefreshCw / TriangleAlert; no other runtime
dependency).

Contract
- Export a forwardRef component whose ref points at the root <div>; props extend
  Omit<React.HTMLAttributes<HTMLDivElement>, "onCopy">.
- The rule set is owned by the component and only seeded by props:
  defaultLength (20), minLength (8), maxLength (64),
  defaultClasses (["lowercase","uppercase","digits","symbols"]),
  defaultExcludeLookalikes (true),
  symbols (a default pool with no backslash, quote or backtick, so the result
  survives shells, CSV and connection strings),
  label ("Generated password" — the accessible name of the value field).
- The password leaves through callbacks, never through a value prop:
  onGenerate(password, { length, poolSize, entropyBits }) after every successful
  generation, onCopy(password) only once the clipboard really accepted it, and
  onCopyError(error) when it did not.
- Every selected class is REQUIRED, not merely allowed. That is what makes a rule
  set impossible to satisfy, and it is the entire reason the refusal state exists.

Behavior
- Randomness comes from crypto.getRandomValues, never Math.random. V8 implements
  Math.random as xorshift128+; its internal state can be recovered from a handful
  of outputs, so passwords drawn from it are reproducible by anyone who sees a few
  of their siblings. If crypto.getRandomValues is missing, refuse and say so — a
  silent downgrade to a predictable PRNG is worse than a generator that admits it
  cannot run here.
- Unbiased indices: draw a uint32 and reject anything at or above
  floor(2**32 / n) * n before taking value % n. A bare modulo gives the first
  (2**32 mod n) indices one extra chance each, biasing every alphabet toward its
  front. Short-circuit n <= 1 to 0 so the loop can never spin on a degenerate
  bound.
- Alphabets: a-z, A-Z, 0-9 and the symbols prop. With "no look-alikes" on, remove
  I l 1 | O 0 o S 5 Z 2 B 8 ` ' " from every pool. De-duplicate each pool and the
  union: a repeated character would be drawn twice as often and would also inflate
  the entropy figure.
- Composition: take one character from each required pool, fill the remainder from
  the de-duplicated union, then Fisher-Yates shuffle using the same rejection-
  sampled indices. Without the shuffle the first N characters are always
  one-per-class in a fixed, guessable order.
- Strength = the entropy of the RULE SET, not a guess made by inspecting the
  string: bits = length * log2(unionPoolSize). Buckets: under 50 Weak, 50-69 Fair,
  70-99 Strong, 100+ Excellent, drawn as a 4-segment meter plus a label plus
  "N bits". (Forcing one character per class shaves a fraction of a bit off the
  ideal figure; the conventional length * log2(pool) is what gets displayed.)
- Three refusals, each of which clears the field — nothing that half-satisfies the
  rules may ever be displayed or copied — and each of which names the knob to turn:
  1. no class selected: "Pick at least one character set — an empty alphabet has
     nothing to draw from.";
  2. a selected class whose pool came out empty (symbols="|" with look-alikes
     excluded, say): name the class and offer both exits (turn it off, or allow
     look-alikes);
  3. length below the number of required classes: "A 3-character password cannot
     hold one character from each of the 4 selected sets. Raise the length to 4,
     or turn a set off."
  Render it in a role="alert" paragraph and put Copy / Regenerate into
  aria-disabled — never the native disabled attribute.
- Generation happens: once on mount inside an effect (NEVER during render — the
  server would emit different characters than the client and hydration would
  fail), on every rule change (memoise a plan from [length, classes,
  excludeLookalikes, symbols] and regenerate when that plan identity changes, so
  dragging the slider streams live values), and on Regenerate.
- Every generation resets the copy state to idle and clears its timer: a "Copied"
  tick sitting next to a value that is no longer on the clipboard is a lie.
- Copy: navigator.clipboard.writeText(password). Success shows a tick and a
  visible "Copied to the clipboard." line for 2s. Failure (insecure context,
  denied permission, no API) select()s the real input and shows "Copy failed — the
  password is selected, press Ctrl/Cmd + C." Never fail silently. Guard the
  handler with a ref that is read AND written synchronously so a double click
  cannot start a second write while the first is in flight, and bail out after the
  await if the component unmounted meanwhile.
- Keyboard. Length slider (a div with role="slider", tabIndex 0): ArrowRight /
  ArrowUp +1, ArrowLeft / ArrowDown -1, PageUp / PageDown +/-8, Home -> minLength,
  End -> maxLength, each with preventDefault so the page does not scroll. Pointer:
  pressing anywhere on the track jumps there, setPointerCapture keeps the drag
  alive outside the track (no window listeners to clean up), touch-none stops a
  touch drag from scrolling the page, and the thumb takes focus on press. The
  character-set chips and the look-alike chip are plain type="button" toggles in
  the natural tab order, activated by Enter and Space.
- ARIA. The value lives in a real readOnly <input>, not a styled <span>: that buys
  native selection, native horizontal scrolling for a 64-character value, and a
  working Ctrl/Cmd+C fallback. Name it with aria-label and point aria-describedby
  at whichever of the strength summary / refusal is currently rendered. The thumb
  carries aria-valuemin / aria-valuemax / aria-valuenow plus aria-valuetext
  "N characters"; the chips carry aria-pressed and keep their visible glyph inside
  the accessible name. Two live regions, both permanently mounted so every
  transition is actually announced: a visible role="status" line for the copy
  result, and an sr-only one for "New password generated. 20 characters, about 124
  bits of entropy." The password itself is never announced — reading it aloud in an
  open-plan office is exactly the leak this component exists to avoid.
- Never put the native disabled attribute on Copy or Regenerate: the browser blurs
  a control the instant it becomes disabled, so switching the last character set
  off while Copy has focus would drop focus onto <body>. Use aria-disabled plus an
  early return in the handler, and leave pointer events on so the state stays
  hoverable, focusable and announced.
- Cleanup: the copy-reset timer, the announcement timer and the flash timer are
  each cleared on unmount and re-armed rather than stacked on the next event; key
  the flash effect on a generation counter (not a boolean) so a burst of
  regenerations restarts it instead of swallowing it.

Rendering & styling
- Semantic tokens only: border + bg-transparent + focus-within:ring-ring for the
  field shell, bg-muted for the slider rail, bg-primary for the filled range and
  the thumb border, border-primary + bg-primary/10 for a pressed chip,
  text-muted-foreground for chrome, text-destructive for refusals and copy
  failures, bg-chart-3 for the Fair segment, bg-chart-2 for Strong / Excellent and
  for the copied tick. No hex, rgb or oklch anywhere.
- The value is font-mono with tracking-wide; the placeholder switches back to the
  sans stack so "No password — fix the rules below" cannot be mistaken for a value.
- Motion is decorative only: the field border flashes border-primary for 600ms
  after each generation through transition-colors, with
  motion-reduce:transition-none — under reduced motion the border simply changes
  and changes back, and nothing about generating, copying or refusing depends on it.
- Reserve the copy-status line's height (min-h-4) so the rules panel never jumps
  when a message appears or clears. Merge the consumer className via cn() on the
  root.

Customization levers
- Alphabets: the letters / digits / default-symbols strings and the LOOKALIKES
  string are plain module constants — swap them for a site policy without touching
  the state machine. A fifth class costs one CLASS_ORDER entry, one CLASS_META
  entry and one branch in poolFor().
- Strength scale: STRENGTH_LEVELS is a 4-row table of { min bits, label, bar
  token, text token }. Move the thresholds for a stricter policy, or add rows —
  the meter renders one segment per row, so the UI follows automatically.
- Density: drop the entropy number, the look-alike hint, or the whole rules panel
  (pass fixed defaults and render only the field row) — planFor() and
  createPassword() are pure and work headless.
- Ranges: minLength / maxLength and the PageUp step define the slider's feel; a
  passphrase variant is the same machine with word-count bounds and a word list as
  the pool.
- Timings and copy: the tick timeout (2s), the announcement window (3s) and the
  flash (600ms) are constants; the failure string is the only place a Ctrl/Cmd key
  name appears, so localising it is a one-line change.

Concepts

  • CSPRNG or nothing — indices come from crypto.getRandomValues through rejection sampling, so every character of every alphabet is equally likely; when the API is absent the component refuses instead of quietly falling back to Math.random, whose state is recoverable from a few outputs.
  • Required classes, not allowed classes — a selected set is guaranteed to appear at least once, which is why the composition step draws one character per pool before filling, and why the shuffle afterwards matters.
  • Refusal is a first-class state — an unsatisfiable rule set (no sets, an empty pool, a length shorter than the number of required sets) clears the field and explains which knob to turn, so a password that only half-obeys the rules can never be copied by accident.
  • Entropy from the rules, not from the stringlength × log2(poolSize) is recomputed as the slider and chips move, so the meter reacts before you even look at the characters; inspecting the produced string would only measure luck.
  • Look-alike exclusion is a trade, and it is priced — dropping I l 1 O 0 … makes a password transcribable but shrinks the alphabet, and the bits readout drops in the same instant so the cost is visible.
  • Generation never happens during render — the first password is produced in a mount effect, so server HTML and client HTML agree; every rule change makes a new plan and the plan is what the effect watches.

On This Page