UI Components Easy
Password Strength
Score password entropy locally and give actionable feedback without sending the secret anywhere.
Open in Lab
MCP
html css javascript react
Targets: TS JS HTML React
Code
:root {
--bg: #0b1020;
--panel: #121a2d;
--line: #263555;
--text: #eef2ff;
--muted: #9eb1d4;
--l0: #2f3d5c;
--l1: #ef4b6b;
--l2: #f0913a;
--l3: #e5cf4a;
--l4: #34d399;
}
*, *::before, *::after { box-sizing: border-box; }
button, input { font: inherit; }
body {
margin: 0;
min-height: 100vh;
display: grid;
place-items: center;
padding: clamp(1rem, 4vw, 3rem);
background: radial-gradient(60rem 34rem at 50% -8%, #17223d 0%, transparent 62%), var(--bg);
color: var(--text);
font: 15px/1.55 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
}
.demo { width: min(28rem, 100%); }
.pw {
background: linear-gradient(180deg, #16203a, var(--panel));
border: 1px solid var(--line);
border-radius: 20px;
padding: clamp(1.1rem, 3vw, 1.75rem);
box-shadow: 0 28px 70px -28px #000a;
}
.pw__title { margin: 0; font-size: 1.25rem; letter-spacing: -0.01em; }
.pw__lede { margin: 0.35rem 0 1.3rem; color: var(--muted); font-size: 0.85rem; }
.pw__field label {
display: block;
font-size: 0.72rem;
text-transform: uppercase;
letter-spacing: 0.09em;
color: var(--muted);
margin-bottom: 0.4rem;
}
.pw__control { position: relative; display: flex; }
.pw__control input {
flex: 1;
min-width: 0;
padding: 0.75rem 4.6rem 0.75rem 0.85rem;
background: #0c1324;
border: 1px solid var(--line);
border-radius: 12px;
color: var(--text);
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
letter-spacing: 0.04em;
outline: none;
transition: border-color 0.2s, box-shadow 0.2s;
}
.pw__control input:focus-visible {
border-color: #78a9ff;
box-shadow: 0 0 0 3px #78a9ff33;
}
.pw__reveal {
position: absolute;
right: 0.4rem;
top: 50%;
transform: translateY(-50%);
background: #1b2740;
border: 1px solid var(--line);
color: var(--muted);
border-radius: 9px;
font-size: 0.75rem;
padding: 0.3rem 0.6rem;
cursor: pointer;
transition: color 0.2s, border-color 0.2s;
}
.pw__reveal:hover { color: var(--text); }
.pw__reveal[aria-pressed="true"] { color: #a9c3ff; border-color: #3d5488; }
.pw__meter { margin: 1.1rem 0 0.95rem; }
.pw__bar { display: grid; grid-template-columns: repeat(4, 1fr); gap: 5px; height: 7px; }
.pw__seg {
background: var(--l0);
border-radius: 99px;
transform: scaleX(1);
transition: background-color 0.35s ease;
}
.pw__bar[data-level="1"] .pw__seg.is-on { background: var(--l1); }
.pw__bar[data-level="2"] .pw__seg.is-on { background: var(--l2); }
.pw__bar[data-level="3"] .pw__seg.is-on { background: var(--l3); }
.pw__bar[data-level="4"] .pw__seg.is-on { background: var(--l4); }
.pw__readout {
display: flex;
justify-content: space-between;
gap: 1rem;
margin-top: 0.5rem;
font-size: 0.8rem;
}
.pw__label { font-weight: 600; color: var(--muted); }
.pw__label[data-level="1"] { color: var(--l1); }
.pw__label[data-level="2"] { color: var(--l2); }
.pw__label[data-level="3"] { color: var(--l3); }
.pw__label[data-level="4"] { color: var(--l4); }
.pw__entropy { color: var(--muted); font-family: ui-monospace, Menlo, monospace; }
.pw__rules {
list-style: none;
margin: 0;
padding: 0.85rem;
background: #0e1628;
border: 1px solid var(--line);
border-radius: 13px;
display: grid;
gap: 0.45rem;
font-size: 0.82rem;
color: var(--muted);
}
.pw__rules li { display: flex; align-items: center; gap: 0.55rem; transition: color 0.2s; }
.pw__rules li.is-met { color: var(--text); }
.pw__dot {
width: 15px;
height: 15px;
flex: none;
border-radius: 50%;
border: 1.5px solid #33436a;
position: relative;
transition: background-color 0.2s, border-color 0.2s;
}
.pw__dot::after {
content: "";
position: absolute;
inset: 0;
opacity: 0;
transition: opacity 0.2s;
background: no-repeat center / 9px 9px
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M1.6 6.4 4.5 9.3 10.4 3' fill='none' stroke='%230b1020' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
}
.pw__rules li.is-met .pw__dot { background: var(--l4); border-color: var(--l4); }
.pw__rules li.is-met .pw__dot::after { opacity: 1; }
.pw__crack { margin: 0.9rem 0 1.1rem; font-size: 0.8rem; color: var(--muted); }
.pw__crack strong { color: var(--text); font-family: ui-monospace, Menlo, monospace; }
.pw__submit {
width: 100%;
padding: 0.8rem;
border: 0;
border-radius: 12px;
font-weight: 600;
color: #fff;
background: linear-gradient(180deg, #5b73ff, #3b4fe0);
cursor: pointer;
transition: filter 0.2s, opacity 0.2s;
}
.pw__submit:hover:not(:disabled) { filter: brightness(1.12); }
.pw__submit:disabled { opacity: 0.4; cursor: not-allowed; }
.pw__submit.is-done { background: linear-gradient(180deg, #34d399, #14a97a); }
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { transition-duration: 0.01ms !important; animation-duration: 0.01ms !important; }
}/**
* Local password entropy scoring.
* Estimates bits of entropy from the effective character pool, then subtracts
* penalties for repeats, sequences, keyboard runs and dictionary words.
* Nothing is ever transmitted.
*/
const form = document.querySelector(".pw");
const input = document.querySelector("#password");
const reveal = document.querySelector(".pw__reveal");
const bar = document.querySelector(".pw__bar");
const segs = [...bar.querySelectorAll(".pw__seg")];
const label = document.querySelector("#strength-label");
const entropyOut = document.querySelector("#entropy-out");
const crackOut = document.querySelector("#crack-time");
const submit = document.querySelector(".pw__submit");
const ruleEls = new Map(
[...document.querySelectorAll(".pw__rules li")].map((li) => [li.dataset.rule, li])
);
const COMMON = [
"password", "passwd", "qwerty", "letmein", "welcome", "admin", "iloveyou",
"dragon", "monkey", "football", "baseball", "master", "sunshine", "princess",
"login", "abc123", "trustno1", "shadow", "superman", "batman", "hello",
];
const ROWS = ["1234567890", "qwertyuiop", "asdfghjkl", "zxcvbnm", "abcdefghijklmnopqrstuvwxyz"];
/** Size of the character pool actually used. */
function poolSize(pw) {
let n = 0;
if (/[a-z]/.test(pw)) n += 26;
if (/[A-Z]/.test(pw)) n += 26;
if (/[0-9]/.test(pw)) n += 10;
if (/[^A-Za-z0-9]/.test(pw)) n += 33;
// Any character outside ASCII widens the pool considerably.
if (/[^\x20-\x7e]/.test(pw)) n += 100;
return n;
}
/** Longest run of characters that follow a keyboard row / alphabet / digits. */
function longestRun(pw) {
const low = pw.toLowerCase();
let best = 0;
for (const row of ROWS) {
const rev = [...row].reverse().join("");
for (const source of [row, rev]) {
for (let i = 0; i < low.length; i++) {
let len = 0;
while (
i + len < low.length &&
source.indexOf(low.slice(i, i + len + 1)) !== -1 &&
len + 1 <= source.length
) len++;
if (len > best) best = len;
}
}
}
return best;
}
/** Longest immediately repeated character or repeated chunk ("abcabc"). */
function repetitionPenalty(pw) {
let penalty = 0;
let run = 1;
for (let i = 1; i < pw.length; i++) {
run = pw[i] === pw[i - 1] ? run + 1 : 1;
if (run >= 3) penalty += 2;
}
for (let size = 1; size <= Math.floor(pw.length / 2); size++) {
const chunk = pw.slice(0, size);
if (chunk.repeat(Math.ceil(pw.length / size)).slice(0, pw.length) === pw) {
penalty += pw.length - size; // "abcabcabc" is barely longer than "abc"
break;
}
}
return penalty;
}
function dictionaryHit(pw) {
const low = pw.toLowerCase().replace(/0/g, "o").replace(/1/g, "l").replace(/3/g, "e").replace(/@/g, "a").replace(/\$/g, "s");
return COMMON.find((w) => low.includes(w)) || null;
}
function analyze(pw) {
const rules = {
length: pw.length >= 12,
case: /[a-z]/.test(pw) && /[A-Z]/.test(pw),
digit: /[0-9]/.test(pw),
symbol: /[^A-Za-z0-9]/.test(pw),
varied: true,
};
const pool = poolSize(pw);
let bits = pw.length > 0 ? pw.length * Math.log2(Math.max(pool, 2)) : 0;
const run = longestRun(pw);
const reps = repetitionPenalty(pw);
const word = dictionaryHit(pw);
const unique = new Set(pw).size;
let advice = "";
if (run >= 4) { bits -= (run - 3) * 4; advice = "Avoid keyboard or alphabet runs."; }
if (reps > 0) { bits -= reps * 2; advice ||= "Avoid repeated characters."; }
if (word) { bits -= 14; advice ||= `“${word}” is a very common word.`; }
if (pw.length > 4 && unique <= Math.ceil(pw.length / 3)) {
bits -= 8;
advice ||= "Use more distinct characters.";
}
rules.varied = pw.length > 0 && run < 4 && reps === 0 && !word;
bits = Math.max(0, Math.round(bits));
let level = 0;
if (bits >= 90) level = 4;
else if (bits >= 60) level = 3;
else if (bits >= 40) level = 2;
else if (bits > 0) level = 1;
if (!advice) {
if (!rules.length) advice = "Length beats complexity — aim for 12+.";
else if (!rules.case) advice = "Mix upper and lower case.";
else if (!rules.digit) advice = "Add a number.";
else if (!rules.symbol) advice = "Add a symbol.";
}
return { bits, level, rules, advice };
}
/** 10^10 guesses/sec offline attack against 2^bits keyspace (half on average). */
function crackTime(bits) {
if (bits === 0) return "—";
const seconds = Math.pow(2, bits - 1) / 1e10;
const units = [
[1, "second"], [60, "minute"], [3600, "hour"], [86400, "day"],
[2592000, "month"], [31536000, "year"], [31536000 * 1000, "millennium"],
];
if (seconds < 1) return "instantly";
let chosen = units[0];
for (const u of units) if (seconds >= u[0]) chosen = u;
const n = seconds / chosen[0];
if (n > 1e6) return `${n.toExponential(1)} ${chosen[1]}s`;
const rounded = n < 10 ? n.toFixed(1) : Math.round(n).toLocaleString();
return `${rounded} ${chosen[1]}${n >= 2 ? "s" : ""}`;
}
const LEVEL_NAMES = ["Enter a password", "Very weak", "Weak", "Good", "Strong"];
function render() {
const pw = input.value;
const { bits, level, rules, advice } = analyze(pw);
bar.dataset.level = String(level);
segs.forEach((seg, i) => seg.classList.toggle("is-on", i < level));
const name = LEVEL_NAMES[level];
label.textContent = pw && advice ? `${name} — ${advice}` : name;
label.dataset.level = String(level);
entropyOut.textContent = `${bits} bits`;
crackOut.textContent = crackTime(bits);
for (const [key, el] of ruleEls) el.classList.toggle("is-met", rules[key]);
submit.disabled = level < 3;
}
input.addEventListener("input", render);
reveal.addEventListener("click", () => {
const shown = input.type === "text";
input.type = shown ? "password" : "text";
reveal.setAttribute("aria-pressed", String(!shown));
reveal.setAttribute("aria-label", shown ? "Show password" : "Hide password");
reveal.textContent = shown ? "Show" : "Hide";
input.focus();
});
form.addEventListener("submit", (event) => {
event.preventDefault();
submit.textContent = "Password accepted";
submit.classList.add("is-done");
});
render();<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Password Strength</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<main class="demo" data-demo="Password Strength">
<form class="pw" novalidate>
<h1 class="pw__title">Create a password</h1>
<p class="pw__lede">Scored locally with entropy math — nothing leaves this page.</p>
<div class="pw__field">
<label for="password">Password</label>
<div class="pw__control">
<input
id="password"
name="password"
type="password"
autocomplete="new-password"
spellcheck="false"
aria-describedby="strength-label entropy-out rules"
/>
<button type="button" class="pw__reveal" aria-pressed="false" aria-label="Show password">Show</button>
</div>
</div>
<div class="pw__meter">
<div class="pw__bar" data-level="0" role="img" aria-label="Strength meter">
<span class="pw__seg"></span><span class="pw__seg"></span><span class="pw__seg"></span><span class="pw__seg"></span>
</div>
<div class="pw__readout">
<span id="strength-label" class="pw__label" role="status" aria-live="polite">Enter a password</span>
<span id="entropy-out" class="pw__entropy">0 bits</span>
</div>
</div>
<ul class="pw__rules" id="rules">
<li data-rule="length"><span class="pw__dot" aria-hidden="true"></span>At least 12 characters</li>
<li data-rule="case"><span class="pw__dot" aria-hidden="true"></span>Upper and lower case</li>
<li data-rule="digit"><span class="pw__dot" aria-hidden="true"></span>A number</li>
<li data-rule="symbol"><span class="pw__dot" aria-hidden="true"></span>A symbol</li>
<li data-rule="varied"><span class="pw__dot" aria-hidden="true"></span>No repeats, keyboard runs or common words</li>
</ul>
<p class="pw__crack">Offline guess time: <strong id="crack-time">—</strong></p>
<button type="submit" class="pw__submit" disabled>Create account</button>
</form>
</main>
<script src="script.js"></script>
</body>
</html>import { useState } from "react";
export function FormPasswordStrength() {
const [value, setValue] = useState("");
return (
<form className="demo" onSubmit={(event) => event.preventDefault()}>
<h2>Password Strength</h2>
<label>
{value.length ? "Ready" : "Enter a value"}
<input value={value} onChange={(event) => setValue(event.target.value)} />
</label>
<button type="submit">Continue</button>
</form>
);
}Password Strength
Score password entropy locally and give actionable feedback without sending the secret anywhere.
Support notes
Validation is local and progressively enhanced. Keep labels, live messages, and inputmode attributes intact when adapting these patterns to a real backend.
Included demo
- Vanilla HTML, CSS, and JavaScript with zero external dependencies.
- React equivalent using the same interaction model.
- A Lab route for trying the behavior in isolation.
Integration checklist
Keep state updates separate from presentation, preserve semantic labels, and add persistence or server callbacks at the boundary where your product needs them.