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…
Installation
npx shadcn@latest add https://ui.zyeon.ai/r/password-generator.jsonPrompt
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.getRandomValuesthrough rejection sampling, so every character of every alphabet is equally likely; when the API is absent the component refuses instead of quietly falling back toMath.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 string —
length × 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.
Assignee Picker
A people picker with searchable presence-aware rows, a first-class Unassigned option, a recently-assigned shortcut group, single or multi assignment, blocked rows that say why, and four data states.
Key Value Editor
An editable list of flat key/value pairs — drag or Alt+Arrow to reorder, switch a row off instead of deleting it, paste a block of Key: value lines, and see both halves of every duplicate key.