Password Strength Meter
stimeo--password-strength
Shows how strong a password is, as guidance, in a meter and in words.
Estimates the strength of the password being typed and shows it both as a meter and in words. The estimate looks at length and the variety of characters; it is not a dictionary check, so treat it as guidance. When you want something stricter, you can hand it a score you worked out yourself. The meter and the wording change immediately, while the spoken message waits until typing stops and only arrives when the level itself changes, so nothing interrupts you mid-word. Because it is told in words as well as colour, it reaches people who cannot rely on the colour.
Type to watch the strength change. The estimate looks at length and character variety rather than checking a dictionary, so treat it as guidance. The spoken message waits until you stop typing, and only when the level changes.
Keyboard
This component has no keyboard interactions of its own.
<%# 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, the 0–1 fill on the
--stimeo--password-strength custom property, and the visible readout — all on the
keystroke that caused them. 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 readout is plain visible output, not a live region: the settled level is
debounced into the shared stimeo--announcer your app seats once, in its layout.
Announcing is opt-in through announce-text-value and only fires when the level
actually changes. The library writes no CSS. %>
<% levels = t("components.password_strength.demo.levels").to_json %>
<% announcement = t("components.password_strength.demo.announcement") %>
<div
class="password-strength"
data-controller="stimeo--password-strength"
data-stimeo--password-strength-levels-value="<%= levels %>"
data-stimeo--password-strength-announce-text-value="<%= announcement %>">
<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"
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 visible readout the level label is written into. Not a live region. | 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. Fewer than two labels fall back to the default scale. Translatable — data-strength is a separate fixed band (default weak, fair, good, strong). |
data-stimeo--password-strength-levels-value |
announceText
|
Template for the spoken message, with {level}, {score}, {max} and {band} placeholders. Leave it unset and nothing is announced. |
data-stimeo--password-strength-announce-text-value |
Actions
| Name | Description | Action |
|---|---|---|
evaluate
|
Re-scores the field; bind it to the input's input event. |
stimeo--password-strength#evaluate |
setScore
|
Adopts a score you computed yourself (action param score or detail.score), clamped into the declared scale; the next evaluate hands scoring back. |
stimeo--password-strength#setScore |
Events
| Name | Description | Event |
|---|---|---|
change
|
Fires on each evaluate or setScore with detail.score, detail.level, detail.max, and detail.meetsMin. |
stimeo--password-strength:change |
reconcile
|
Fires once after a runtime value or target change, and only when the derived state above actually moved. | stimeo--password-strength:reconcile |
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. Both ends stay anchored: the weakest score is always weak and the strongest always strong. |
data-below-min |
Root element | Present while a non-empty password scores under minScore (never, with the default minScore=0). |
--stimeo--password-strength |
Root element | The 0–1 fill your CSS turns into the bar width. |