A fully accessible, responsive password generator built with React 19 and TypeScript. Users can configure character length and character types, generate a secure password, see its strength rating, and copy it to the clipboard — all from a single polished interface.
- Overview
- Screenshots
- Built With
- What I Learned
- Project Structure
- Getting Started
- Roadmap
- AI Collaboration
- Acknowledgments
- Author
This is a solution to the Password Generator App challenge on Frontend Mentor.
Users should be able to:
- Generate a password based on selected character-type options (uppercase, lowercase, numbers, symbols)
- Control password length via an interactive range slider (1–20 characters)
- Copy the generated password to the clipboard with one click
- See a real-time strength rating (Too Weak / Weak / Medium / Strong) for the current configuration
- View an optimal layout on any screen size (mobile-first, responsive at 540 px and 1024 px breakpoints)
- See hover, focus, and active states for all interactive elements
- Use the app with a keyboard only (fully accessible form controls and focus management)
- Live Site: https://fsdev-password-generator-app.vercel.app
- Solution: https://github.com/gusanchefullstack/fsdev-password-generator-app
| Layer | Technology |
|---|---|
| UI library | React 19 |
| Language | TypeScript 6 |
| Build tool | Vite 8 |
| Styling | CSS Modules + CSS custom properties |
| Layout | Flexbox, mobile-first responsive design |
| Fonts | JetBrains Mono Variable (self-hosted) |
| Deployment | Vercel |
A naive random password can fail to include characters from every selected set. The solution guarantees at least one character per active set, then fills remaining slots randomly, then shuffles the entire array:
const safeGuaranteed = guaranteedChars.slice(0, length); // clamp to requested length
const remaining = length - safeGuaranteed.length;
const randomChars = Array.from({ length: remaining }, () =>
charset[Math.floor(Math.random() * charset.length)]
);
const allChars = [...safeGuaranteed, ...randomChars];
// Fisher-Yates shuffle
for (let i = allChars.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[allChars[i], allChars[j]] = [allChars[j], allChars[i]];
}Key insight: slicing guaranteedChars to length prevents the output from exceeding the requested character count when length < number of active sets.
Styling a range slider's fill requires a dynamic CSS custom property (--fill-percent). Inline styles are an accessibility anti-pattern; injecting a <style> tag inside a sectioning element is an HTML error. The fix uses useId() for a unique scope class and createPortal to place the <style> tag in <head>:
const uid = useId();
const scopeClass = `slider-${uid.replace(/:/g, '')}`;
return (
<>
{createPortal(
<style>{`.${scopeClass}{--fill-percent:${percentage}%}`}</style>,
document.head
)}
<div className={`${styles.container} ${scopeClass}`}>
...
</div>
</>
);This satisfies the HTML validator, avoids inline style attributes, and keeps the CSS variable scoped to the specific slider instance.
The generated password field uses <output> (not <p> or <div>), which:
- Has implicit ARIA role
statusfor live region announcements - Accepts a
forattribute linking it to the form controls that computed it - Is keyboard-focusable with
tabIndex={0}
Grouping all controls in a <form> with onSubmit means the Generate button works as type="submit", enabling the Enter key to trigger generation natively without extra JavaScript event wiring.
When useId() generates a dynamic ID inside CharacterLengthSlider, the <output>'s for attribute in PasswordDisplay would be stale if each component generated its own ID independently. The solution lifts ID generation to App.tsx and passes the ID as a prop to both components:
// App.tsx
const sliderId = useId();
const sliderInputId = `char-length-${sliderId.replace(/:/g, '')}`;
<CharacterLengthSlider inputId={sliderInputId} ... />
<PasswordDisplay sliderInputId={sliderInputId} ... />All breakpoints use em units so they respect the user's browser font-size preference:
/* 540px ÷ 16 = 33.75em */
@media (min-width: 33.75em) { ... }
/* 1024px ÷ 16 = 64em */
@media (min-width: 64em) { ... }- MDN —
<output>element - MDN —
prefers-reduced-motion - CSS Tricks — Custom range slider styling
- WCAG 2.1 — Understanding SC 1.4.3 Contrast (Minimum)
- Fisher-Yates Shuffle — Wikipedia
src/
├── components/
│ ├── CharacterLengthSlider/ # Range slider with dynamic fill via CSS custom property
│ ├── GenerateButton/ # Submit button that triggers the parent form
│ ├── OptionCheckbox/ # Accessible custom checkbox for each character type
│ ├── PasswordDisplay/ # <output> element with copy-to-clipboard action
│ └── StrengthMeter/ # Visual bar-chart strength indicator
├── hooks/
│ └── usePasswordGenerator.ts # Core state: password, options, strength, clipboard
├── styles/
│ ├── global.css # Reset, body, sr-only, focus-visible
│ ├── tokens.css # CSS custom properties (colors, typography, spacing)
│ └── responsive.css # App-level layout at breakpoints
├── App.tsx # Composition root, ID lifting, form wiring
└── main.tsx # React DOM entry point
Prerequisites: Node.js ≥ 18
# Clone
git clone https://github.com/gusanchefullstack/fsdev-password-generator-app.git
cd fsdev-password-generator-app
# Install
npm install
# Develop
npm run dev # Vite dev server → http://localhost:5173
# Build
npm run build # TypeScript check + Vite production build → dist/
# Preview production build
npm run previewNo environment variables required — the app is entirely client-side.
- Password generation with guaranteed character inclusion
- Fisher-Yates shuffle for uniform randomness
- Real-time strength meter (4 levels)
- Copy to clipboard (Clipboard API +
execCommandfallback) - Accessible range slider with dynamic CSS fill
- Responsive layout (mobile / tablet / desktop)
- Full keyboard navigation and screen-reader support
- Reduced-motion support (
prefers-reduced-motion) - Deployed to Vercel
- Exclude ambiguous characters option (e.g.,
0,O,l,1) - Password history (last 5 generated)
- Entropy calculation displayed alongside strength
This project was built in collaboration with Claude Code (claude-sonnet-4-6) via the Anthropic CLI.
How AI was used:
- Scaffolding the Vite + React + TypeScript project structure and component architecture
- Implementing the Fisher-Yates shuffle with guaranteed character inclusion logic
- Solving the dynamic CSS custom property problem (
createPortal+useId()approach) - Iteratively fixing accessibility, HTML, CSS, and JavaScript issues flagged by the Frontend Mentor quality report (score improved to 8.8/10 Exceptional)
- Writing semantic HTML (
<output>,<form>,<fieldset>,<legend>) and scoped focus styles
What worked well: Using Claude to reason through non-obvious patterns (like lifting IDs for <output for> correctness) and to cross-reference WCAG contrast ratios.
What required human judgment: Deciding which linter warnings were false positives (e.g., prefers-reduced-motion: no-preference pattern, root-relative asset paths on a root-deployed app) vs. genuinely worth fixing.
- Frontend Mentor for the challenge design and quality report tooling
- JetBrains for the open-source JetBrains Mono variable font
- The WCAG working group for clear contrast-ratio guidance
Gustavo Sanchez

