Password strength
Password strength is a visual and assistive-technology indicator of how well a password satisfies a set of configurable requirements, graded into levels (four by default: Weak, Fair, Good, Strong) shown as filled segments plus an always-visible level name, since color is never the only signal. It's a pure, one-way indicator: set its value to whatever a password field (or any other source) currently holds, since this component never reads or writes that field itself.
Matching rules are configurable at two levels: a shared pool of testable requirements, and a description of how many of them must match for a given level to be reached, or, per level, an entirely different set of requirements of its own.
When to use
Reach for password strength next to a password field, to give someone real-time feedback while they type, such as during account creation or a "change your password" form. It's purely a read-out: nothing about it enforces the password meeting any level before the surrounding form can be submitted. That's still your form's own validation to wire up, matching the same requirements.
Install & usage
Pick a framework in the toolbar above and these snippets adapt.
npm install @eunomia/elements
import "@eunomia/elements/password-strength.js";
<eun-password-strength value="Abcdefg1!"></eun-password-strength>
Importing the file registers <eun-password-strength> as a custom element,
with no further setup needed. It works with any framework, or none, since
it's a standard web component.
npm install @eunomia/elements
<script type="module">
import "@eunomia/elements/password-strength.js";
</script>
<eun-input id="password" type="password" label="Password"></eun-input>
<eun-password-strength id="strength"></eun-password-strength>
<script type="module">
const password = document.getElementById("password");
const strength = document.getElementById("strength");
password.addEventListener("eunchange", () => {
strength.value = password.value;
});
</script>
npm install @eunomia/elements
import { useState } from "react";
import "@eunomia/elements/password-strength.js";
function PasswordField() {
const [password, setPassword] = useState("");
return (
<>
<eun-input
type="password"
label="Password"
value={password}
oneunchange={(event) => setPassword(event.target.value)}
/>
<eun-password-strength value={password} />
</>
);
}
npm install @eunomia/elements
"use client";
import { useState } from "react";
import "@eunomia/elements/password-strength.js";
export function PasswordField() {
const [password, setPassword] = useState("");
return (
<>
<eun-input
type="password"
label="Password"
value={password}
oneunchange={(event) => setPassword(event.target.value)}
/>
<eun-password-strength value={password} />
</>
);
}
npm install @eunomia/elements
<script setup>
import { ref } from "vue";
import "@eunomia/elements/password-strength.js";
const password = ref("");
</script>
<template>
<eun-input
type="password"
label="Password"
:value="password"
@eunchange="password = $event.target.value"
></eun-input>
<eun-password-strength :value="password"></eun-password-strength>
</template>
npm install @eunomia/elements
import { CUSTOM_ELEMENTS_SCHEMA, Component } from "@angular/core";
import "@eunomia/elements/password-strength.js";
@Component({
selector: "app-password-field",
template: `
<eun-input
type="password"
label="Password"
[value]="password"
(eunchange)="password = $event.target.value"
></eun-input>
<eun-password-strength [value]="password"></eun-password-strength>
`,
schemas: [CUSTOM_ELEMENTS_SCHEMA],
})
export class PasswordFieldComponent {
password = "";
}
Guidance
- Keep the same
rulesboth here and in the password field's ownvalidators, so the meter and the actual form validation never disagree about what "Strong" means - Order
levelsfrom weakest to strongest, since a level's own condition (rulesorminScore) is only ever checked to potentially raise the reached index, never lower it - Reach for a level's own
ruleswhen it needs a specific pattern (e.g. "must contain the app name" for a demo/test account) rather than just "enough" matched requirements
- Treating a "Strong" reading as proof the password is safe: this is a UX nudge based on configurable, superficial pattern checks, not a real strength estimator (no dictionary/breach-list/entropy analysis)
- Relying on
hide-labelwithout providing your own equally accessible, always-visible text elsewhere, since the segments alone are decorative and colors are never announced - Skipping real server-side password policy enforcement because the meter reads "Strong" client-side
Live testing
Properties
Password strength <eun-password-strength>
Attributes
| Name | Type | Default | Description |
|---|---|---|---|
| hide-label | boolean | false | Hides the always-visible caption and level name. The level name is still announced to screen readers on every change either way |
| value | string | '' | The password to grade |
| label | string | 'Password strength' | A static caption shown next to the current level's name, and used to prefix the message announced to screen readers on every level change |
| rules | Array<PasswordStrengthRule> | DEFAULT_PASSWORD_STRENGTH_RULES | The shared pool of requirements levels are scored against by default. Four by default: eight or more characters, upper and lower case, one number, and one special character |
| levels | Array<PasswordStrengthLevel> | DEFAULT_PASSWORD_STRENGTH_LEVELS | The levels to render, ordered from weakest to strongest. Four by default: Weak, Fair, Good, and Strong |
Import the exact TypeScript type behind any property above, see
CSS custom properties
| Name | Description |
|---|---|
| --password-strength-gap | Sets the gap between the segment row and the label |
| --password-strength-segment-gap | Sets the gap between individual segments |
| --password-strength-track-height | Sets the height of the segments |
| --password-strength-border-radius | Sets the corner radius of the segments |
| --password-strength-track-color | Sets the color of the unfilled segments |
| --password-strength-fill-color | Sets a fallback color for a level with no severity |
| --password-strength-critical-color | Sets the color for the critical severity |
| --password-strength-warning-color | Sets the color for the warning severity |
| --password-strength-info-color | Sets the color for the info severity |
| --password-strength-success-color | Sets the color for the success severity |
| --password-strength-transition-duration | Sets the duration of the segments' fill transition |
| --password-strength-font-size | Sets the font size of the caption and level name |
| --password-strength-caption-color | Sets the color of the static caption text |
| --password-strength-level-font-weight | Sets the font weight of the level name |
Basic
value (default '') is graded against the default 4 levels (Weak / Fair
/ Good / Strong), scored on the default rule pool (8+ characters, upper +
lower case, a digit, a special character).
<eun-password-strength value="a"></eun-password-strength>
<eun-password-strength value="abcdefg1"></eun-password-strength>
<eun-password-strength value="Abcdefg1"></eun-password-strength>
<eun-password-strength value="Abcdefg1!"></eun-password-strength>
Wired to a real field
eun-password-strength never reads a field itself, so forward value on
every change.
<eun-input id="password" type="password" label="Password"></eun-input>
<eun-password-strength id="strength" label="Strength"></eun-password-strength>
<script type="module">
const password = document.getElementById("password");
const strength = document.getElementById("strength");
password.addEventListener("eunchange", () => {
strength.value = password.value;
});
</script>
Custom rules
rules is a JS-only property (regexes have no HTML attribute form), so
set it directly on the element.
document.querySelector("eun-password-strength").rules = [
{ label: "At least 12 characters", regex: /.{12,}/ },
{ label: "At least 20 characters", regex: /.{20,}/ },
{ label: "Contains a space (passphrase style)", regex: /\s/ },
];
Custom levels, per-level rules
levels is also JS-only. Each level defaults to scoring against the shared
rules pool (minScore defaults to its 1-based position), but can define
its own rules instead, evaluated independently, ignoring the shared
score entirely.
document.querySelector("eun-password-strength").levels = [
{ label: "Too short", severity: "critical" },
{
label: "Passphrase",
severity: "success",
// Its own rule, ignoring the shared `rules` pool's score entirely:
// at least 4 space-separated words.
rules: [{ regex: /^(\S+\s+){3,}\S+$/ }],
},
];
Only 3 levels
levels isn't locked to 4, since the segment row always matches its length.
document.querySelector("eun-password-strength").levels = [
{ label: "Weak", severity: "critical" },
{ label: "Medium", severity: "warning" },
{ label: "Strong", severity: "success" },
];
Hidden label
hide-label keeps only the segment row, for layouts rendering their own
text elsewhere. Remember to keep that replacement text just as visible,
since color is never announced (see the Accessibility tab).
<eun-password-strength value="Abcdefg1!" hide-label></eun-password-strength>
Custom
Override the --password-strength-* CSS variables, listed in full in
the API tab.
<eun-password-strength
value="Abcdefg1!"
class="custom-strength"
></eun-password-strength>
.custom-strength {
--password-strength-track-height: 10px;
--password-strength-border-radius: 2px;
--password-strength-track-color: #ede9fe;
--password-strength-success-color: #7c3aed;
--password-strength-font-size: 13px;
}
Color is never the only signal
The segment row is purely decorative (aria-hidden="true"). The real,
always-visible content is the plain-text caption plus level name next to it
(e.g. "Password strength: Fair"), satisfied without relying on any ARIA
widget role. Setting hide-label removes that text, so only do so once
an equally visible replacement exists elsewhere. Otherwise, color-blind
and low-vision users lose the only accessible signal the component
provides.
Level changes are announced automatically
Unlike eun-progress-bar's native <meter> (whose value changes aren't
announced on their own), eun-password-strength pairs a visually-hidden
role="status" aria-live="polite" region with value, updated only when
the reached level actually changes, so screen reader users hear "Password
strength: Weak", then "Password strength: Strong", etc. as they type,
with no extra wiring needed on your side.
Reference links