Password Strength Meter
stimeo--password-strength
Scores a password with a lightweight heuristic and drives a meter + label.
The stimeo--password-strength controller estimates password strength with a zero-dependency heuristic (length milestones and character-class variety, capped for trivial repetition) and pairs with Password Reveal to round out the sign-up experience. There is no dedicated APG pattern; the meter display follows Meter. On each input it syncs the meter's aria-valuenow, reflects the level token on data-strength and the 0–1 fill on --stimeo-password-strength, and writes the level into a polite aria-live label (debounced so a screen reader is not flooded mid typing). The estimate is intentionally not a dictionary/zxcvbn-grade one; swap a stronger estimator in on the consumer side if needed. Behavior only — the look is owned by this Playground.
Type into the field to see the strength update. The estimate is a lightweight heuristic (length and character variety) — not a dictionary check — so treat it as guidance, not a guarantee. The meter value and label update immediately, while the spoken label is debounced to avoid interrupting a screen reader while you type.
<%# Markup for the password-strength demo.
The controller scores the field with a lightweight zero-dependency heuristic and
drives the meter (aria-valuenow), the data-strength token, and the 0–1 fill on the
--stimeo-password-strength custom property. The input sits inside the controller so
its data-action input->...#evaluate binds directly. Level labels are localized via
the levels value; demo.css colors the bar by the locale-independent data-strength band,
so the look never depends on the (translated) label text. The library writes no CSS. %>
<% levels = t("components.password_strength.demo.levels").to_json %>
<div
class="password-strength"
data-controller="stimeo--password-strength"
data-stimeo--password-strength-levels-value="<%= levels %>">
<label class="password-strength__label" for="password-strength-demo-input">
<%= t("components.password_strength.demo.label") %>
</label>
<input
class="password-strength__input"
id="password-strength-demo-input"
type="password"
autocomplete="new-password"
aria-describedby="password-strength-demo-readout"
data-stimeo--password-strength-target="input"
data-action="input->stimeo--password-strength#evaluate">
<div
class="password-strength__meter"
role="meter"
aria-valuemin="0"
aria-valuemax="4"
aria-valuenow="0"
aria-label="<%= t('components.password_strength.demo.meter_label') %>"
data-stimeo--password-strength-target="meter">
<span class="password-strength__bar"></span>
</div>
<span
class="password-strength__readout"
id="password-strength-demo-readout"
aria-live="polite"
data-stimeo--password-strength-target="label"></span>
</div>
<p class="password-strength__hint"><%= t("components.password_strength.demo.hint") %></p>
/*
* Presentation-only styles for the password-strength demo.
* The bar width comes from --stimeo-password-strength (0–1); its color comes from
* the data-strength band (weak…strong), which is locale-independent — so the look
* holds even though the visible level label is localized via the levels value.
*/
.password-strength {
display: flex;
flex-direction: column;
gap: 0.5rem;
max-width: 24rem;
}
.password-strength__label {
font-weight: 600;
}
.password-strength__input {
padding: 0.5rem 0.75rem;
border: 1px solid var(--border-strong);
border-radius: 0.375rem;
background: var(--bg);
font: inherit;
color: var(--fg);
}
.password-strength__input:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 1px;
}
.password-strength__meter {
height: 0.5rem;
border-radius: 0.375rem;
background: var(--surface-subtle);
overflow: hidden;
}
.password-strength__bar {
display: block;
height: 100%;
border-radius: inherit;
width: calc(var(--stimeo-password-strength, 0) * 100%);
background: var(--border-interactive);
transition:
width 0.2s ease,
background-color 0.2s ease;
}
/* Color bands keyed on the locale-independent data-strength token (weak → strong). */
.password-strength[data-strength="weak"] .password-strength__bar {
background: var(--danger-500);
}
.password-strength[data-strength="fair"] .password-strength__bar {
background: var(--amber-500);
}
.password-strength[data-strength="good"] .password-strength__bar {
background: var(--amber-500);
}
.password-strength[data-strength="strong"] .password-strength__bar {
background: var(--leaf-500);
}
.password-strength__readout {
min-height: 1.25rem;
font-size: 0.85rem;
color: var(--muted);
}
.password-strength__hint {
margin: 0.25rem 0 0;
max-width: 32rem;
font-size: 0.85rem;
line-height: 1.5;
color: var(--muted);
}
@media (prefers-reduced-motion: reduce) {
.password-strength__bar {
transition: none;
}
}
This demo needs no consumer-side JS (the controller handles the behavior).
These demo styles use shared design tokens (light + dark). Copy the shared styles too, then toggle data-theme on your root element for dark mode.
The data-* attributes you add to your own HTML to wire this component. Put the data-controller below on a root element, then place its targets / values / actions inside that element.
On the root element
data-controller="stimeo--password-strength"
Targets
| Name | Description | Attribute |
|---|---|---|
input
required
|
The password field whose value is scored. | data-stimeo--password-strength-target="input" |
meter
required
|
The meter element whose aria-valuenow mirrors the level. | data-stimeo--password-strength-target="meter" |
label
|
The polite live region the level token is written into. | data-stimeo--password-strength-target="label" |
Values
| Name | Description | Attribute |
|---|---|---|
minScore
|
Score below which the password is treated as failing; surfaced as the data-below-min hook and meetsMin=false in the change event; 0 (default) disables the gate. |
data-stimeo--password-strength-min-score-value |
levels
|
Display labels in ascending order (shown in the label and the change event's level); the count sets the scale. Translatable — data-strength is a separate fixed band (default weak, fair, good, strong). |
data-stimeo--password-strength-levels-value |
Actions
| Name | Description | Action |
|---|---|---|
evaluate
|
Re-scores the field; bind it to the input's input event. |
stimeo--password-strength#evaluate |
Events
| Name | Description | Event |
|---|---|---|
change
|
Fires on each evaluate with detail.score, detail.level, detail.max, and detail.meetsMin. |
stimeo--password-strength:change |
State hooks
The library only manages these ARIA/data attributes and custom properties. Your CSS reads them to render the look — selectors like [aria-selected], [aria-expanded], or var(--stimeo--…) hook into this state.
| Hook | Target | Meaning |
|---|---|---|
aria-valuenow |
meter target | The strength level (0 = empty … number of levels). |
data-strength |
Root element | A fixed, locale-independent band (weak / fair / good / strong) — stable for CSS even when levels is translated; absent when empty. |
data-below-min |
Root element | Present while the score is under minScore (never, with the default minScore=0). |
--stimeo-password-strength |
Root element | The 0–1 fill your CSS turns into the bar width. |