Password Strength
A segmented password strength meter that provides real-time feedback during account creation and password updates.
For entropy-aware scoring with advanced dictionaries and pattern detection, compute a score externally (e.g. with zxcvbn) and pass it through the score attribute.
Features
4-segment progress bar with semantic levels (weak → fair → good → strong) Built-in heuristic scoring — length + character variety External score override via scoreattribute (0..4)Custom level labels via labelsattributeAccessible meter semantics — role="meter",aria-valuenow,aria-valuetextLive label updates via aria-live="polite"Themeable through CSS custom properties
Source Code
View Source Code
import { define, html, prop } from '@vielzeug/craft';
import { computed } from '@vielzeug/ripple';
import { reducedMotionMixin } from '../../styles';
import componentStyles from './password-strength.css?inline';
/** Scoring levels for password strength. */
export type PasswordStrengthLevel = 'empty' | 'weak' | 'fair' | 'good' | 'strong';
/** Props accepted by <sg-password-strength>. */
export type SgPasswordStrengthProps = {
/** Accessible name for assistive technology. Default: 'Password strength'. */
label?: string;
/**
* Optional level labels in order: empty, weak, fair, good, strong.
* If omitted or invalid length, defaults are used.
*/
labels?: string[];
/**
* Optional score override (0..4). Use this to integrate external scorers
* such as zxcvbn while keeping block rendering and accessibility behavior.
* -1 means no override (default).
*/
score?: number;
/** Whether to render visible textual feedback. Default: true. */
'show-label'?: boolean;
/** Password string to evaluate. */
value?: string;
};
/**
* Strong password meter with segmented progress visualization.
*
* Built-in scoring is heuristic and conservative:
* - length < 6 => weak
* - length >= 8 with mixed case => fair
* - + digit or symbol => good
* - length >= 12 with mixed case, digit and symbol => strong
*
* @element sg-password-strength
*
* @attr {string} value - Password string to evaluate
* @attr {number} score - Optional score override (0..4). Use -1 for no override (default: -1)
* @attr {boolean} show-label - Show visible feedback label (default: true)
* @attr {string} label - Accessible name (default: 'Password strength')
*
* @cssprop --password-strength-height Segment bar height
* @cssprop --password-strength-gap Gap between segments
* @cssprop --password-strength-radius Segment corner radius
* @cssprop --password-strength-track-bg Inactive segment color
* @cssprop --password-strength-label-size Visible label font size
* @cssprop --password-strength-label-color Visible label color
* @cssprop --password-strength-weak-color Weak state segment color
* @cssprop --password-strength-fair-color Fair state segment color
* @cssprop --password-strength-good-color Good state segment color
* @cssprop --password-strength-strong-color Strong state segment color
*
* @example
* ```html
* <!-- Pair with an sg-input to evaluate in real time -->
* <sg-input type="password" label="Password" name="password" id="pwd"></sg-input>
* <sg-password-strength id="meter"></sg-password-strength>
* <script type="module">
* document.getElementById('pwd').addEventListener('input', (e) => {
* document.getElementById('meter').value = e.target.value;
* });
* </script>
*
* <!-- Fixed score display (e.g. from a server-side score) -->
* <sg-password-strength score="3"></sg-password-strength>
* ```
*/
export const PASSWORD_STRENGTH_TAG = 'sg-password-strength' as const;
define<SgPasswordStrengthProps>(PASSWORD_STRENGTH_TAG, {
props: {
label: prop.string('Password strength'),
labels: prop.json(undefined as string[] | undefined),
score: prop.number(-1),
'show-label': prop.bool(true),
value: prop.string(),
},
setup(props, { bind, el: _el }) {
const defaultLabels: Record<PasswordStrengthLevel, string> = {
empty: '',
fair: 'Fair',
good: 'Good',
strong: 'Strong',
weak: 'Weak',
};
const levels: PasswordStrengthLevel[] = ['empty', 'weak', 'fair', 'good', 'strong'];
const computeScore = (password: string): 0 | 1 | 2 | 3 | 4 => {
if (!password) return 0;
if (password.length < 6) return 1;
const hasLower = /[a-z]/.test(password);
const hasUpper = /[A-Z]/.test(password);
const hasDigit = /\d/.test(password);
const hasSymbol = /[^a-zA-Z0-9]/.test(password);
const long = password.length >= 12;
if (long && hasLower && hasUpper && hasDigit && hasSymbol) return 4;
if ((hasLower || hasUpper) && (hasDigit || hasSymbol) && password.length >= 8) return 3;
if ((hasLower || hasUpper) && password.length >= 8) return 2;
return 1;
};
const computeLevel = (): PasswordStrengthLevel => {
const external = props.score.value ?? -1;
const finalScore =
external >= 0 ? Math.max(0, Math.min(4, Math.trunc(external))) : computeScore(props.value.value ?? '');
return levels[finalScore];
};
const score = computed<0 | 1 | 2 | 3 | 4>(() => {
// score >= 0 means an external override was provided
const external = props.score.value ?? -1;
if (external >= 0) {
return Math.max(0, Math.min(4, Math.trunc(external))) as 0 | 1 | 2 | 3 | 4;
}
return computeScore(props.value.value ?? '');
});
const levelLabel = computed<string>(() => {
const custom = props.labels.value;
if (Array.isArray(custom) && custom.length === 5) return String(custom[score.value] ?? '');
return defaultLabels[computeLevel()];
});
const ariaValueText = computed<string | null>(() => {
if (score.value === 0) return null;
return levelLabel.value || null;
});
// Sync level change to data-level attribute reactively
bind({
attr: {
'data-level': () => computeLevel(),
},
});
const segClass = (threshold: number) => () => `segment${score.value >= threshold ? ' active' : ''}`;
return html`
<div
class="meter"
role="meter"
:aria-label="${props.label}"
aria-valuemin="0"
aria-valuemax="4"
:aria-valuenow="${() => String(score.value)}"
:aria-valuetext="${() => ariaValueText.value}">
<div class="segments" aria-hidden="true">
<div class="${segClass(1)}"></div>
<div class="${segClass(2)}"></div>
<div class="${segClass(3)}"></div>
<div class="${segClass(4)}"></div>
</div>
</div>
${() =>
props['show-label'].value
? html`<span class="level-label" aria-live="polite" aria-atomic="true">${() => levelLabel.value}</span>`
: ''}
`;
},
styles: [reducedMotionMixin, componentStyles],
});Basic Usage
<sg-password-strength value="Tr0ub4dor&3"></sg-password-strength>Common Registration Flow
Bind the meter to a password input by forwarding the input event's value.
External Scoring
Pass a normalized score from your own scoring engine. Set score to 0–4; the built-in heuristic is bypassed.
Custom Level Labels
Override the default level strings (Weak, Fair, Good, Strong) by providing all five labels in order: empty, weak, fair, good, strong.
Bar Only (No Visible Label)
Set show-label="false" to render only the visual segments while preserving the full meter semantics for screen readers.
How the Built-in Scorer Works
The component uses a conservative heuristic based on length and character variety. The rules, from weakest to strongest:
| Score | Level | Condition |
|---|---|---|
0 | empty | No value |
1 | weak | Length < 6 |
2 | fair | Length ≥ 8 with mixed case |
3 | good | Length ≥ 8 with mixed case + digit or symbol |
4 | strong | Length ≥ 12 with mixed case, digit, and symbol |
For production use, prefer an external scorer like zxcvbn and pass the result via score.
API Reference
Attributes
| Attribute | Type | Default | Description |
|---|---|---|---|
value | string | — | Password string to evaluate with the built-in heuristic |
score | number | -1 | External score override 0..4; -1 means use built-in scorer |
show-label | boolean | true | Show visible textual level feedback below the bar |
label | string | Password strength | Accessible name (aria-label) on the meter element |
labels | string[] | Built-in level names | All five level labels: [empty, weak, fair, good, strong] |
Parts
| Part | Description |
|---|---|
| (none) | No shadow parts are exposed |
CSS Custom Properties
| Property | Default | Description |
|---|---|---|
--password-strength-height | 0.375rem | Segment bar height |
--password-strength-gap | var(--space-1) | Gap between segments |
--password-strength-radius | var(--rounded-full) | Segment corner radius |
--password-strength-track-bg | var(--color-contrast-300) | Inactive segment background color |
--password-strength-track-border | var(--color-contrast-400) | Inactive segment border color |
--password-strength-label-size | var(--text-sm) | Visible label font size |
--password-strength-label-color | currentColor | Visible label color |
--password-strength-weak-color | var(--color-warning-500) | Active color for weak score |
--password-strength-fair-color | var(--color-warning-600) | Active color for fair score |
--password-strength-good-color | var(--color-success-500) | Active color for good score |
--password-strength-strong-color | var(--color-success-600) | Active color for strong score |
Accessibility
- Uses
role="meter"witharia-valuemin="0",aria-valuemax="4", and dynamicaria-valuenow. - Provides human-readable state through
aria-valuetext(Weak,Fair,Good,Strong). - When
scoreis0(empty),aria-valuetextis omitted to avoid announcing "empty".
- The visible label uses
aria-live="polite"andaria-atomic="true"to announce level transitions.
- The shimmer transition respects
prefers-reduced-motion: reduce.
- Decorative segments are hidden from the accessibility tree via
aria-hidden="true".