# Password Strength Meter

> A password strength meter: a four-segment bar sweeps in and drains, rule ticks draw as each is met, with a confirm field match tick.

Canonical: https://gsapvault.com/ui-elements/password-strength-meter
Live demo: https://gsapvault.com/demos/password-strength-meter/index.html

| Property | Value |
|----------|-------|
| Type | ui-element |
| Tier | paid |
| Price | £5 |
| Difficulty | intermediate |
| Plugins | Core GSAP only |
| Techniques | micro-interaction, form-animation, form-validation, svg-line-draw, stagger, keyboard-navigation |
| Uses Lenis | No |

## Lighthouse, as measured

Google Lighthouse on the demo, 15 September 2026. A measurement of the demo as shipped, not a promise for your page.

| Category | Score |
|----------|-------|
| Accessibility | 100 |
| Best practices | 100 |

No performance score is published: it depends on the deployment (server compression, caching, CDN, connection and device) rather than on the code, so measure it where it will live.

## Overview

A sign-up password field that shows its working. As the visitor types, a four-segment meter fills with a short left-to-right sweep inside each segment, and drains back in reverse when a character comes out. The strength label beside it rolls up to Weak, Fair, Good or Strong and rolls down again on the way back, and each rule in the checklist draws a tick in its circle the moment it is met and un-draws it when it is lost. A confirm field below stays quiet while it is still a prefix of the password, then shows a mismatch or draws a match tick, and the Create account button only enables once the password is Strong and confirmed.

The scoring is real and deterministic: length and character variety as an estimate in bits, a built-in list of the most common passwords (with letter swaps and bolted-on digits undone), and penalties for repeats and runs such as abcd, 4321 or qwer. It is also pluggable, so a buyer can hand the score to zxcvbn with one attribute. The field is a native password input with an eye toggle that keeps the caret where it was, the strength is announced politely once typing pauses, and each rule states met or not met to screen readers. Ships in light and dark, switched with one data-variant attribute.

## Features

- Four-segment meter: each segment sweeps in left to right, and drains in reverse
- Strength label rolls up or down between Weak, Fair, Good and Strong
- Rule ticks draw themselves in when met and un-draw when lost
- Deterministic scoring: bits from length and variety, a common-password list, penalties for repeats and runs
- Pluggable scorer: name a function in data-scorer (zxcvbn drops straight in)
- Eye toggle is a button with aria-pressed that keeps focus and the caret position
- Confirm field waits while it is still a prefix, then shows mismatch or a drawn match tick
- Create account stays disabled until the password is Strong and confirmed
- Strength announced through a debounced polite live region, rules expose met or not met in text
- Light and dark themes on one attribute, status colours muted and used only for strength

## Use Cases

- Sign-up and account creation forms
- Password reset and change-password screens
- Invitation acceptance flows that set a first password
- Admin consoles that enforce a password policy

## Vibe-Code Ready Setup

This UI element includes `START-HERE-AI.md`, a product-specific copy-paste setup prompt for Cursor, Claude Code, ChatGPT, GitHub Copilot, Windsurf, and other coding assistants. It tells the assistant to inspect the existing stack, integrate the supplied files, preserve the design, scope selectors, retain accessibility and responsive behaviour, add framework-appropriate GSAP cleanup, and report what it tested.

[How AI-assisted setup works](https://gsapvault.com/vibe-coding)

## How It Works

### Sweep in, drain out

Each segment holds a fill scaled on x from its left edge. When the score rises, only the newly earned segments tween to scaleX 1, one after another from the left; when it falls, the segments above the new score tween back to 0 from the right. The colour comes from a data-level attribute on the meter, so the status tokens stay in CSS.

### A label that always tells the truth

The visible label is updated immediately, because the input's aria-describedby reads it. An aria-hidden copy of the old word is laid over it and rolls out while the new one rolls in, upward for stronger and downward for weaker. A separate live region speaks the strength and the count of met rules only after typing has paused, so it never chatters per keystroke.

### Scoring you can read and replace

The built-in scorer counts length minus predictable characters (the third and later of a repeat or a run) and multiplies by log2 of the character pool: under 30 bits is Weak, under 45 Fair, under 60 Good, otherwise Strong. A common password is Weak regardless, fewer than eight characters is Weak, and Strong needs every rule in the checklist. Any function named in data-scorer replaces the estimate; the checklist caps still apply.

## Documentation

How this UI element works and how it goes into a page. The reference you use once you own the files (worked examples, events, the programmatic API, the class list) ships with the download.

### What's Included

- `index.html`: the demo page and the markup to copy
- `assets/style.css`: component styles, plain CSS custom properties for colours
- `assets/script.js`: readable, commented source with an `onReady` guard

- Each newly earned segment sweeps in from its own left edge, one after another; lost segments drain right to left
- The strength label rolls up for stronger and down for weaker, and is never mid-swap for a screen reader
- Rule ticks are drawn with `stroke-dashoffset` from each path's measured length, and un-drawn when a rule is lost
- Real, deterministic scoring with a common-password list and penalties for repeats and runs, documented below and replaceable with one attribute
- The eye toggle keeps the value, the focus and the caret position across the type swap
- The confirm field says nothing while it is still a prefix of the password, then shows a mismatch or draws a match tick
- The submit button is disabled until the password is Strong, every rule is met and the confirm field matches
- Native `<input type="password">`, `aria-pressed`, `aria-describedby`, `aria-invalid` and a polite live region

### Quick Start

**1. Add to your `<head>`:**

_Code snippet omitted: it ships with the download._

**2. Add before the closing `</body>` tag:**

_Code snippet omitted: it ships with the download._

**3. Copy the `<form class="psm" data-psm>` block from `index.html` into your page.**

The script looks only at `data-` hooks inside each `[data-psm]` root, so several can share a page:

| Hook | On | Required | What it is |
|---|---|---|---|
| `data-psm` | the form (or any wrapper) | yes | One component instance |
| `data-psm-input` | the password input | yes | The field that is scored |
| `data-psm-toggle` | a `<button>` | no | The eye toggle; `data-psm-slash` marks the slash path it draws |
| `data-psm-meter` | the meter row | no | Receives `data-level="0"` to `"4"` |
| `data-psm-fill` | four spans | no | The segment fills; any count works |
| `data-psm-label` | a span | no | The strength word; give it the `id` the input's `aria-describedby` names |
| `data-psm-live` | a visually hidden span | no | The debounced announcement |
| `data-rule="length\|case\|number\|symbol\|common"` | each `<li>` | no | A checklist rule; `data-rule-tick` marks its tick path and `data-rule-state` its hidden "Met: / Not met: " text |
| `data-psm-confirm` | the confirm input | no | Enables the match state; `data-psm-match-icon` and `data-psm-match` are its tick and message |
| `data-psm-submit` | the submit button | no | Disabled until valid; `data-psm-submit-text` holds its label |
| `data-min-length` | the root | no | Minimum length for the length rule, default `12` (edit the rule's text to match) |
| `data-scorer` | the root | no | Name of a global scoring function, see Scoring |
| `data-psm-demo` | the root | no | Demo only: a valid submit is confirmed in place instead of sent |

Leave out `data-psm-demo` in production and a valid form submits normally. An invalid submit (Enter in the field) is cancelled and focus goes back to the password.

The demo toolbar and the `.stage` wrapper in `index.html` are demo furniture, not part of the component; leave them behind. `assets/script.js` reads no `.stage-*` class.

### Keyboard & Accessibility

| Key | What it does |
|-----|--------------|
| Typing / `Backspace` | Rescores on every input event; the meter, label and ticks follow |
| `Tab` | Password, eye toggle, confirm password, Create account (skipped while disabled) |
| `Space` / `Enter` on the eye | Shows or hides the password; the input keeps its value and caret |
| `Enter` in a field | Submits when valid; otherwise cancelled with focus returned to the password |

- **The password input** has `aria-describedby="psmStrength psmRules"`, pointing at the strength word and the checklist, so both are read when it takes focus. The visible word is always the current one: the rolling copy of the old word is `aria-hidden`.
- **Announcements**: a visually hidden `aria-live="polite"` region speaks "Password strength: Good. 3 of 5 requirements met." only after 900ms without input, and only when that sentence changes, so it never chatters per keystroke. The visible label is not itself live.
- **Rules** carry hidden text that switches between "Met: " and "Not met: ", so the state is never colour or icon alone. The tick marks are `aria-hidden`.
- **The eye toggle** is a `<button type="button">` labelled "Show password" with `aria-pressed` for its state. A pointer press is cancelled on `pointerdown` so focus stays in the input, and the selection is restored after the type swap.
- **The confirm field** gets `aria-invalid="true"` on a mismatch and is described by its message, which is itself a polite live region.
- **The submit button** uses the native `disabled` attribute until valid.
- `prefers-reduced-motion: reduce`: the same end states with every duration at zero, read live so a change while the page is open applies at once.
- Without JavaScript the form is a plain pair of password inputs with `required` and `minlength="12"`, and the submit button stays enabled.

### Scoring

The built-in scorer is `PasswordStrengthMeter.score(value)`, returning `0` (empty) to `4`:

1. **Common passwords are Weak.** The value is lower-cased, common letter swaps are undone (`P@ssw0rd`), and trailing digits and symbols are stripped (`Password123!`); if any of those forms is on the built-in list, the score is 1.
2. **Predictable characters count for nothing.** The third and later character of a repeat (`aaa`) or of a run (`abcd`, `4321`, or `qwer` along a keyboard row) is removed from the length.
3. **Bits = remaining length x log2(character pool)**, where the pool adds 26 for lower case, 26 for upper case, 10 for digits and 33 for symbols. Under 30 bits is Weak, under 45 Fair, under 60 Good, and 60 or more Strong.

The component then applies two caps whatever scorer is in use, because the checklist is the contract the visitor can see: **fewer than 8 characters, or a common password, is Weak**, and **Strong needs every rule met**. The rules are at least `data-min-length` characters (12), upper and lower case, a number, a symbol, and not a common password.

**Swap in your own scorer.** Name a global function in `data-scorer`, or assign `PasswordStrengthMeter.scorer`. It receives the value and returns a number from 0 to 4 or an object with a `score` property. zxcvbn returns exactly that:

_Code snippet omitted: it ships with the download._

A non-empty value is always at least 1, so zxcvbn's 0 shows as Weak. Extend the common list with `PasswordStrengthMeter.common.add('yourcompanyname')`. Scoring in the browser is guidance for the visitor; enforce your policy on the server as well.

**Events and API.** Every change dispatches `psm:change` on the root with `detail` `{ score, label, rules, matches, valid }`. A valid submit dispatches a cancelable `psm:submit`; call `preventDefault()` on it to send the form yourself. The root also carries `root.passwordStrengthMeter` with `set(value, confirmValue)`, `reset()`, `state()` and `destroy()`, which removes every listener.

### How It Works

**Sweep in, drain out.** Each `.psm-fill` is scaled on `x` from its left edge. When the score rises, only the segments not already full get a `gsap.to(fill, { scaleX: 1 })`, delayed one after another from the left; when it falls, the segments above the new score tween to 0 from the right. Every tween uses `overwrite: true`, so fast typing never leaves a segment stranded half full. The fill colour comes from `data-level` on the meter, so the status colours never enter JavaScript.

**The rolling label.** The label's text is replaced at once, then it tweens from `yPercent: 100` (or `-100` when weakening) to 0 while an `aria-hidden` ghost holding the old word tweens out the other way and removes itself. A fresh change kills any ghost still rolling before it starts.

**Drawn ticks.** Each tick's length is read once with `getTotalLength()` and used as its `strokeDasharray`; met tweens `strokeDashoffset` to 0, lost tweens it back. A round-capped dash of zero length still paints a dot, so a fully un-drawn tick is set to `opacity: 0`. The eye's slash and the confirm field's match tick use the same helper.

### Using It With Your Own Design

- **The fills must carry no CSS `transform`.** GSAP owns their `scaleX`; the script sets the empty start state, so nothing is pre-hidden without JavaScript.
- `.psm-strength` is a one-line window with `overflow: hidden` and a fixed width; widen it if your words are longer than "Strong".
- `.psm-seg` has `overflow: hidden` so a fill sweeping in stays inside its rounded track.
- The labels, rule wording, button text and fonts are yours. Keep each rule's `data-rule` key; the text beside it is free.

### Themes

One design at two token values, `light` (the default) and `dark`. Pick one by setting the attribute on `<body>` in the demo, or on the component's wrapper in your page:

_Code snippet omitted: it ships with the download._

Nothing else changes: same markup, same script, and no theme name is read in JavaScript. The demo's toolbar toggle and the `?variant=dark` URL parameter only set that attribute.

Every colour, radius and shadow is a custom property defined in both `body[data-variant="..."]` blocks in `assets/style.css`:

- **Ground and ink**: `--ground`, `--ground-2`, `--raised`, `--hover`, `--ink`, `--ink-2`, `--ink-3`, `--line`, `--line-strong`, `--shadow`
- **Accent** (focus ring, pressed eye, submit button only): `--accent`, `--accent-ink`, `--accent-text`, `--accent-wash`
- **Status** (the meter and its label only): `--danger-fill`, `--warn-fill`, `--ok-fill` for segments and ticks, and `--danger-text`, `--warn-text`, `--ok-text` for the word, each measured at 4.5:1 or better on the ground
- **Controls**: `--seg-empty`, `--button-off`, `--button-off-ink`
- **Type and radii**: `--font`, `--radius`, `--radius-sm`, `--radius-xs`

To re-value it for your brand, change those properties in the two blocks. The accent here is `#5e6ad2` with white text at 4.7:1; check that pair if you swap it, and update the submit button's hover colour in `.psm-submit:hover` to match. Weak is danger, Fair is warning, Good and Strong are success: strength is status, so the accent never appears on the meter.

### Customisation

- `FILL`, `FILL_STEP`, `DRAIN`, `DRAIN_STEP`, `ROLL`, `DRAW` and `UNDRAW` at the top of the init in `assets/script.js` are the durations and staggers.
- `ANNOUNCE_DELAY` (900ms) is how long typing must pause before the strength is spoken.
- The bit thresholds (30 / 45 / 60) are in `defaultScore`, and the common list is the `COMMON` array above it.
- Change `data-min-length` and the length rule's wording together.

### Requirements

- GSAP 3.12+ (core only, no plugins)
- No build step, no framework

## What You Get

- `index.html`: working demo page
- `assets/script.js`: commented, readable source
- `assets/style.css`: effect styles
- `README.md`: full documentation with examples and framework integration notes
- `START-HERE-AI.md`: product-specific copy-paste prompt for AI-assisted setup
- `LICENSE.txt`: standard license terms
- Lifetime updates: re-download anytime from your library

## Get the Code

This is a premium UI element. The standard license costs £5 one-time and covers unlimited personal and commercial projects with no attribution required for our code. Bundled third-party assets retain their own licences and attribution requirements. The only restrictions: no redistribution of the code itself and no competing effect libraries.

- [Buy Password Strength Meter](https://gsapvault.com/ui-elements/password-strength-meter)
- [The Vault (£99 one-time, best value): every collection in the Vault library, plus future items added to those collections](https://gsapvault.com/effects)

---

From [GSAP Vault](https://gsapvault.com): production-ready GSAP animation effects. Full catalog for agents: https://gsapvault.com/llms-full.txt
