What You'll Build
A password generator that creates secure random passwords using the browser's built-in cryptography API. Control the length, choose which character types to include, and see the password strength in real time.
- Adjustable password length (8–64 characters)
- Toggles for uppercase, lowercase, numbers, symbols
- Auto-regenerate on any change
- Live strength meter (Weak → Very Strong)
- Copy to clipboard with one click
- Uses
crypto.getRandomValues()— cryptographically secure
🔐 Password Generator
Create strong, secure passwords
What You'll Learn
- Web Crypto API:
crypto.getRandomValues()— the only truly secure way to generate randomness in the browser - Why
Math.random()is unsafe: it's predictable and should never be used for passwords - Entropy: how password strength is mathematically calculated
- Building character pools: combining charset strings based on user choices
- Clipboard API:
navigator.clipboard.writeText()with success feedback - Range inputs: live-updating sliders
Key new skill: Knowing the difference between random and cryptographically secure random. If you're generating passwords, tokens, or keys, you must use crypto.getRandomValues(). Never Math.random().
Build the HTML Structure
Create index.html. The layout has an output box, a strength meter, a length slider, four option toggles, and a generate button.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Password Generator</title>
</head>
<body>
<div class="card">
<h1>Password Generator</h1>
<p class="sub">Create strong, secure passwords</p>
<div class="output-box">
<div class="password" id="password">Click Generate</div>
<button class="copy-btn" id="copyBtn">Copy</button>
</div>
<div class="strength">
<div class="strength-bar">
<div class="strength-fill" id="strengthFill"></div>
</div>
<div class="strength-label">
<span>Strength:</span>
<span class="value" id="strengthText">—</span>
</div>
</div>
<div class="field">
<div class="field-header">
<label>Password length</label>
<span class="length-value" id="lengthValue">16</span>
</div>
<input type="range" id="length" min="8" max="64" value="16">
</div>
<div class="options">
<label class="option checked">
<input type="checkbox" id="upper" checked>
<span class="label">ABC Uppercase</span>
</label>
<label class="option checked">
<input type="checkbox" id="lower" checked>
<span class="label">abc Lowercase</span>
</label>
<label class="option checked">
<input type="checkbox" id="numbers" checked>
<span class="label">123 Numbers</span>
</label>
<label class="option checked">
<input type="checkbox" id="symbols" checked>
<span class="label">!@# Symbols</span>
</label>
</div>
<button class="regenerate-btn" id="generateBtn">Generate New Password</button>
</div>
<script src="script.js"></script>
</body>
</html>
Structure note: Each option is a <label> wrapping a <input type="checkbox">. This means clicking anywhere in the label toggles the checkbox — a small UX improvement that users appreciate.
Style It with CSS
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: #f1f5f9;
padding: 20px;
color: #0f172a;
min-height: 100vh;
}
.card {
max-width: 480px;
margin: 0 auto;
background: #fff;
border-radius: 16px;
padding: 24px;
box-shadow: 0 10px 40px rgba(15,23,42,0.08);
}
h1 { font-size: 1.3rem; margin-bottom: 4px; }
.sub { color: #64748b; font-size: 0.85rem; margin-bottom: 18px; }
/* Output box with password + copy button */
.output-box {
background: #f8fafc;
border: 2px solid #e2e8f0;
border-radius: 12px;
padding: 18px 20px;
margin-bottom: 16px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
}
.password {
flex: 1;
font-family: "Courier New", monospace;
font-size: 1.15rem;
font-weight: 700;
color: #1d4ed8;
word-break: break-all;
letter-spacing: 0.5px;
}
.copy-btn {
background: #3b82f6;
color: #fff;
border: none;
padding: 8px 14px;
border-radius: 8px;
font-weight: 700;
font-size: 0.82rem;
cursor: pointer;
transition: 0.2s;
font-family: inherit;
flex-shrink: 0;
}
.copy-btn:hover { background: #2563eb; }
.copy-btn.copied { background: #16a34a; }
/* Strength meter */
.strength { margin-bottom: 18px; }
.strength-bar {
height: 8px;
background: #e2e8f0;
border-radius: 999px;
overflow: hidden;
margin-bottom: 8px;
}
.strength-fill {
height: 100%;
width: 0%;
transition: width 0.3s, background 0.3s;
border-radius: 999px;
}
.strength-label {
display: flex;
justify-content: space-between;
font-size: 0.75rem;
color: #64748b;
}
.strength-label .value { font-weight: 700; }
/* Length field */
.field { margin-bottom: 16px; }
.field-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
label { font-size: 0.82rem; font-weight: 600; color: #334155; }
.length-value {
background: #eff6ff;
color: #1d4ed8;
padding: 3px 10px;
border-radius: 6px;
font-family: "Courier New", monospace;
font-weight: 700;
font-size: 0.85rem;
}
input[type="range"] {
width: 100%;
accent-color: #3b82f6;
cursor: pointer;
}
/* Option toggles */
.options {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
margin-bottom: 16px;
}
.option {
background: #f8fafc;
border: 2px solid #e2e8f0;
border-radius: 10px;
padding: 12px 14px;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
transition: 0.2s;
user-select: none;
}
.option:hover { border-color: #cbd5e1; }
.option.checked {
background: #eff6ff;
border-color: #3b82f6;
}
.option input {
accent-color: #3b82f6;
width: 16px;
height: 16px;
cursor: pointer;
}
.option .label {
font-size: 0.82rem;
font-weight: 600;
color: #334155;
}
.option.checked .label { color: #1d4ed8; }
/* Generate button */
.regenerate-btn {
width: 100%;
padding: 13px;
background: #3b82f6;
color: #fff;
border: none;
border-radius: 10px;
font-weight: 700;
font-size: 0.9rem;
cursor: pointer;
transition: 0.2s;
font-family: inherit;
}
.regenerate-btn:hover { background: #2563eb; }
Write the JavaScript — Secure Random
The most important function in this project. This is why we don't use Math.random().
// ============================================================
// CHARACTER SETS — building blocks for passwords
// ============================================================
var CHARSETS = {
upper: "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
lower: "abcdefghijklmnopqrstuvwxyz",
numbers: "0123456789",
symbols: "!@#$%^&*()_+-=[]{}|;:,.<>?"
};
// ============================================================
// SECURE RANDOM — the critical function
// ============================================================
// Math.random() is NOT secure. It's predictable — given enough
// outputs, an attacker can predict the next one.
//
// crypto.getRandomValues() pulls from the OS's cryptographic
// entropy source. It's the only safe way to generate passwords.
function secureRandom(max) {
var arr = new Uint32Array(1);
crypto.getRandomValues(arr);
return arr[0] % max;
}
// ============================================================
// GRAB DOM ELEMENTS
// ============================================================
var passwordEl = document.getElementById("password");
var lengthEl = document.getElementById("length");
var lengthValueEl = document.getElementById("lengthValue");
var generateBtn = document.getElementById("generateBtn");
var copyBtn = document.getElementById("copyBtn");
var strengthFill = document.getElementById("strengthFill");
var strengthText = document.getElementById("strengthText");
var options = ["upper", "lower", "numbers", "symbols"];
Never use Math.random() for passwords, tokens, or API keys. It's designed for games and animations, not security. An attacker who sees a few outputs can predict future ones. Always use crypto.getRandomValues().
Write the JavaScript — Generate & Strength
// ============================================================
// GENERATE PASSWORD
// ============================================================
function generatePassword() {
var length = parseInt(lengthEl.value);
// Build a pool by combining all selected charsets
var pool = "";
options.forEach(function(opt) {
if (document.getElementById(opt).checked) {
pool += CHARSETS[opt];
}
});
// Nothing selected — show a hint
if (!pool) {
passwordEl.textContent = "Select at least one option";
updateStrength(0);
return "";
}
// Build the password, one character at a time
var password = "";
for (var i = 0; i < length; i++) {
password += pool[secureRandom(pool.length)];
}
passwordEl.textContent = password;
updateStrength(computeStrength(pool.length, length));
return password;
}
// ============================================================
// STRENGTH — measured in bits of entropy
// ============================================================
// Entropy = length x log2(poolSize)
// Higher is better. Think of it as "how many guesses would
// an attacker need to crack this?"
//
// < 40 bits = Weak (crackable in minutes)
// < 60 bits = Fair (crackable in days)
// < 80 bits = Good (crackable in months)
// < 120 bits = Strong (crackable in centuries)
// >= 120 bits = Very Strong (uncrackable with current tech)
function computeStrength(poolSize, length) {
var entropy = length * Math.log2(poolSize);
if (entropy < 40) return 1;
if (entropy < 60) return 2;
if (entropy < 80) return 3;
if (entropy < 120) return 4;
return 5;
}
function updateStrength(level) {
var labels = ["—", "Weak", "Fair", "Good", "Strong", "Very Strong"];
var colors = ["#e2e8f0", "#ef4444", "#f59e0b", "#fbbf24", "#22c55e", "#16a34a"];
var pct = [0, 20, 40, 60, 80, 100][level];
strengthFill.style.width = pct + "%";
strengthFill.style.background = colors[level];
strengthText.textContent = labels[level];
strengthText.style.color = colors[level];
}
Entropy explained: A password with entropy = 80 bits means an attacker would need to try 2^80 combinations — that's a trillion trillion guesses. Modern hardware can crack ~10 billion guesses per second, so 80 bits takes ~1,000+ years. That's why 16 characters is a good default.
Write the JavaScript — Events & Clipboard
// ============================================================
// EVENT LISTENERS
// ============================================================
// Length slider — update the label and regenerate
lengthEl.addEventListener("input", function() {
lengthValueEl.textContent = lengthEl.value;
generatePassword();
});
// Option checkboxes — toggle the visual state and regenerate
options.forEach(function(opt) {
var checkbox = document.getElementById(opt);
var label = checkbox.closest(".option");
checkbox.addEventListener("change", function() {
label.classList.toggle("checked", checkbox.checked);
generatePassword();
});
});
// Generate button
generateBtn.addEventListener("click", generatePassword);
// ============================================================
// COPY TO CLIPBOARD
// ============================================================
// The Clipboard API is async and requires user interaction.
// We show "Copied!" feedback for 1.5 seconds.
copyBtn.addEventListener("click", function() {
var text = passwordEl.textContent;
if (!text || text === "Click Generate") return;
navigator.clipboard.writeText(text).then(function() {
copyBtn.textContent = "✅ Copied!";
copyBtn.classList.add("copied");
setTimeout(function() {
copyBtn.textContent = "📋 Copy";
copyBtn.classList.remove("copied");
}, 1500);
}).catch(function() {
// Fallback if clipboard is blocked (rare)
alert("Please copy manually: " + text);
});
});
// ============================================================
// BOOT
// ============================================================
generatePassword();
Save all files and open index.html. A strong password generates immediately. Change the length, toggle options, and watch the strength meter update. Click Copy to save it.
Understand the Big Ideas
1. Why Math.random() is unsafe
Math.random() returns a number between 0 and 1. But it's not truly random — it starts from a "seed" and uses a mathematical formula to produce the next value. If an attacker knows the seed (or sees enough outputs), they can predict every future value.
This is fine for a dice-roll animation. It's catastrophic for a password. Real cryptography needs unpredictable randomness from a hardware source — that's what crypto.getRandomValues() provides.
2. Building a character pool
The password is built one character at a time, each drawn from a pool of allowed characters. If the user selects uppercase and numbers, the pool is A–Z + 0–9 = 36 characters. Each character in the password is chosen randomly from this pool.
// For each character position
for (var i = 0; i < length; i++) {
// Pick a random index from 0 to pool.length-1
var randomIndex = secureRandom(pool.length);
// Append that character
password += pool[randomIndex];
}
3. Understanding entropy
Strength isn't about how "messy" a password looks — it's about how many guesses it takes to crack. That's measured in bits of entropy:
entropy = length x log2(pool size) Length 16, pool 94 chars (all selected): 16 x log2(94) = 16 x 6.55 = 104.8 bits => "Strong" Length 8, pool 26 chars (only lowercase): 8 x log2(26) = 8 x 4.7 = 37.6 bits => "Weak"
Longer passwords with larger character pools are exponentially harder to crack. This is why "length" matters more than "complexity".
4. The Clipboard API
navigator.clipboard.writeText(text) is a modern browser API that writes to the system clipboard. It returns a Promise, so you chain .then() for success and .catch() for failure.
Privacy: Some browsers require HTTPS and a user interaction (click, keypress) before allowing clipboard access. This prevents malicious scripts from silently stealing what's on your clipboard.
Practice Challenges
🟢 Beginner
- Add a memorable passphrase mode — use a word list instead of random characters
- Add a password history showing the last 5 generated
- Add an exclude similar characters option (removes
0/O,1/l/I)
🟡 Intermediate
- Ensure at least one character from each selected charset (currently random chance)
- Add a crack-time estimate — "would take 3 billion years to crack"
- Add a QR code — generate a QR that opens a password manager
🔴 Advanced
- Add PWA support — installable on desktop and mobile
- Add offline mode with a service worker
- Add password strength analyzer — check an existing password's entropy
Compare your solution: We built the production version with similar features — see the live Password Generator.
Full Source Code
Common Mistakes & Fixes
❌ "The password looks the same every time"
You're using Math.random(). Switch to crypto.getRandomValues(). Even if the password looks random, Math.random() outputs are predictable.
❌ "crypto.getRandomValues is not defined"
The Crypto API is only available on HTTPS pages (or localhost). If you're opening the file as file://, some browsers block it. Serve the page over HTTP, or use a simple Python server: python -m http.server.
❌ "Copy button doesn't work"
The Clipboard API requires a secure context (HTTPS) and a user gesture (click). If you're running from file://, the copy silently fails. Test in a real HTTP server.
❌ "The password contains characters I didn't select"
Check that the charset merging is correct. The pool should only contain characters from selected charsets. Log pool to the console to debug.
❌ "Strength meter shows 'Very Strong' for a weak password"
You might be miscalculating entropy. Make sure you use Math.log2() (base-2 log), not Math.log() (base-e). Only base-2 gives bits.