A password strength meter scores a password as the user types and turns that score into immediate visual feedback. Build the meter as a React component whose input state drives four checks, four bars, a color, and a text label.
Implement the default App component in App.tsx. Keep the password in React state and derive a numeric score from it during render.
0: no bars are filled and the label reads Too weak.abcdefgh passes only the length check, so it scores 1 and reads Weak.Abcdefg1 passes length, mixed case, and digit checks, so it scores 3 and reads Good.Abcdefg1! passes all four checks, so all bars turn green and the label reads Strong.password in useState; do not store a second score state that can drift.value and onChange on the password input.sN meter class, the filled-bar count, and the label lookup.You will hold the password in useState, derive a single score from it on each render, and let that one number drive the bars, the color, and the label.
A strength meter reacts to every keystroke. Type a longer password and a bar lights up; add a digit and another fills; add a symbol and it goes green and reads "Strong". All of that visible change is one number, score, shown three ways.
Do not think of "how many bars", "which color", and "the label" as three pieces of state. They are one thing — the score — rendered three ways. Compute score from the password, and the class, the fill color, and the text all fall out of it. Because score is derived on render, it can never disagree with the password.
A common first try stores the score in its own state and updates it inside the change handler:
const [password, setPassword] = useState('');
const [score, setScore] = useState(0);
function handleChange(e) {
setPassword(e.target.value);
// forgot to recompute score here — or computed it from stale `password`
}
Now there are two sources of truth. If any code path updates password without also recomputing score, the meter lies. And reading password inside handleChange gives you the value from the previous render. Derived data should be computed during render, not stored.
import { useState } from 'react';
import './styles.css';
const LABELS = ['Too weak', 'Weak', 'Fair', 'Good', 'Strong'];
function scorePassword(pw: string): number {
let score = 0;
if (pw.length >= 8) score++;
if (/[a-z]/.test(pw) && /[A-Z]/.test(pw)) score++;
if (/[0-9]/.test(pw)) score++;
if (/[^A-Za-z0-9]/.test(pw)) score++;
return score;
}
export default function App() {
const [password, setPassword] = useState('');
const score = scorePassword(password);
return (
<main className="container">
<h1>Password Strength</h1>
<input
type="password"
autoComplete="new-password"
className="input"
placeholder="Enter a password"
aria-label="Password"
aria-describedby="strength-label"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<div className={score > 0 ? `meter s${score}` : 'meter'}>
{[0, 1, 2, 3].map((i) => (
<span key={i} className={i < score ? 'bar on' : 'bar'} />
))}
</div>
<p id="strength-label" className="label" aria-live="polite">
{LABELS[score]}
</p>
</main>
);
}
password is the only state. score is recomputed on every render by scorePassword, so it is always in step with what is typed. The meter class gets s${score} (CSS uses that to pick the color), the first score bars get the on class, and the label is a plain array lookup. aria-describedby connects the field to that label, while aria-live="polite" announces changes without interrupting other speech.
The four checks are independent, so a password can earn points in any order. These concrete states show how the score, label, and bars stay together.
Abcdefg1: length is 8 (+1), it has lower and upper (+1), it has a digit (+1), no symbol (+0) so score = 3.meter s3, bars 0,1,2 get on (bar 3 stays empty), and .meter.s3 .bar.on colors them blue.LABELS[3] which is "Good". Add a ! and score becomes 4: all four bars fill, turn green, label reads "Strong".score in state — it can drift from password. Derive it during render so the two are always consistent.e.target.value vs password — score from e.target.value if you must compute in the handler; password there is the previous render's value.value={password} the field is not controlled, so React state and the DOM can disagree. Wire both value and onChange.type between password and text is the natural next feature.This version routes password edits through a reducer and counts a reusable list of criteria. The rendered meter remains identical.
import { useReducer } from 'react';
import './styles.css';
const LABELS = ['Too weak', 'Weak', 'Fair', 'Good', 'Strong'];
const RULES = [
(value: string) => value.length >= 8,
(value: string) => /[a-z]/.test(value) && /[A-Z]/.test(value),
(value: string) => /[0-9]/.test(value),
(value: string) => /[^A-Za-z0-9]/.test(value),
];
function scorePassword(value: string) {
return RULES.filter((rule) => rule(value)).length;
}
export default function App() {
const [password, updatePassword] = useReducer(
(_current: string, next: string) => next,
'',
);
const score = scorePassword(password);
return (
<main className="container">
<h1>Password Strength</h1>
<input
type="password"
autoComplete="new-password"
className="input"
placeholder="Enter a password"
aria-label="Password"
aria-describedby="strength-label"
value={password}
onChange={(event) => updatePassword(event.target.value)}
/>
<div className={score > 0 ? `meter s${score}` : 'meter'}>
{[0, 1, 2, 3].map((index) => (
<span key={index} className={index < score ? 'bar on' : 'bar'} />
))}
</div>
<p id="strength-label" className="label" aria-live="polite">
{LABELS[score]}
</p>
</main>
);
}Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.
A password strength meter scores a password as the user types and turns that score into immediate visual feedback. Build the meter as a React component whose input state drives four checks, four bars, a color, and a text label.
Implement the default App component in App.tsx. Keep the password in React state and derive a numeric score from it during render.
0: no bars are filled and the label reads Too weak.abcdefgh passes only the length check, so it scores 1 and reads Weak.Abcdefg1 passes length, mixed case, and digit checks, so it scores 3 and reads Good.Abcdefg1! passes all four checks, so all bars turn green and the label reads Strong.password in useState; do not store a second score state that can drift.value and onChange on the password input.sN meter class, the filled-bar count, and the label lookup.You will hold the password in useState, derive a single score from it on each render, and let that one number drive the bars, the color, and the label.
A strength meter reacts to every keystroke. Type a longer password and a bar lights up; add a digit and another fills; add a symbol and it goes green and reads "Strong". All of that visible change is one number, score, shown three ways.
Do not think of "how many bars", "which color", and "the label" as three pieces of state. They are one thing — the score — rendered three ways. Compute score from the password, and the class, the fill color, and the text all fall out of it. Because score is derived on render, it can never disagree with the password.
A common first try stores the score in its own state and updates it inside the change handler:
const [password, setPassword] = useState('');
const [score, setScore] = useState(0);
function handleChange(e) {
setPassword(e.target.value);
// forgot to recompute score here — or computed it from stale `password`
}
Now there are two sources of truth. If any code path updates password without also recomputing score, the meter lies. And reading password inside handleChange gives you the value from the previous render. Derived data should be computed during render, not stored.
import { useState } from 'react';
import './styles.css';
const LABELS = ['Too weak', 'Weak', 'Fair', 'Good', 'Strong'];
function scorePassword(pw: string): number {
let score = 0;
if (pw.length >= 8) score++;
if (/[a-z]/.test(pw) && /[A-Z]/.test(pw)) score++;
if (/[0-9]/.test(pw)) score++;
if (/[^A-Za-z0-9]/.test(pw)) score++;
return score;
}
export default function App() {
const [password, setPassword] = useState('');
const score = scorePassword(password);
return (
<main className="container">
<h1>Password Strength</h1>
<input
type="password"
autoComplete="new-password"
className="input"
placeholder="Enter a password"
aria-label="Password"
aria-describedby="strength-label"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<div className={score > 0 ? `meter s${score}` : 'meter'}>
{[0, 1, 2, 3].map((i) => (
<span key={i} className={i < score ? 'bar on' : 'bar'} />
))}
</div>
<p id="strength-label" className="label" aria-live="polite">
{LABELS[score]}
</p>
</main>
);
}
password is the only state. score is recomputed on every render by scorePassword, so it is always in step with what is typed. The meter class gets s${score} (CSS uses that to pick the color), the first score bars get the on class, and the label is a plain array lookup. aria-describedby connects the field to that label, while aria-live="polite" announces changes without interrupting other speech.
The four checks are independent, so a password can earn points in any order. These concrete states show how the score, label, and bars stay together.
Abcdefg1: length is 8 (+1), it has lower and upper (+1), it has a digit (+1), no symbol (+0) so score = 3.meter s3, bars 0,1,2 get on (bar 3 stays empty), and .meter.s3 .bar.on colors them blue.LABELS[3] which is "Good". Add a ! and score becomes 4: all four bars fill, turn green, label reads "Strong".score in state — it can drift from password. Derive it during render so the two are always consistent.e.target.value vs password — score from e.target.value if you must compute in the handler; password there is the previous render's value.value={password} the field is not controlled, so React state and the DOM can disagree. Wire both value and onChange.type between password and text is the natural next feature.This version routes password edits through a reducer and counts a reusable list of criteria. The rendered meter remains identical.
import { useReducer } from 'react';
import './styles.css';
const LABELS = ['Too weak', 'Weak', 'Fair', 'Good', 'Strong'];
const RULES = [
(value: string) => value.length >= 8,
(value: string) => /[a-z]/.test(value) && /[A-Z]/.test(value),
(value: string) => /[0-9]/.test(value),
(value: string) => /[^A-Za-z0-9]/.test(value),
];
function scorePassword(value: string) {
return RULES.filter((rule) => rule(value)).length;
}
export default function App() {
const [password, updatePassword] = useReducer(
(_current: string, next: string) => next,
'',
);
const score = scorePassword(password);
return (
<main className="container">
<h1>Password Strength</h1>
<input
type="password"
autoComplete="new-password"
className="input"
placeholder="Enter a password"
aria-label="Password"
aria-describedby="strength-label"
value={password}
onChange={(event) => updatePassword(event.target.value)}
/>
<div className={score > 0 ? `meter s${score}` : 'meter'}>
{[0, 1, 2, 3].map((index) => (
<span key={index} className={index < score ? 'bar on' : 'bar'} />
))}
</div>
<p id="strength-label" className="label" aria-live="polite">
{LABELS[score]}
</p>
</main>
);
}Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.