Skip to content

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 score attribute (0..4)
  • Custom level labels via labels attribute
  • Accessible meter semanticsrole="meter", aria-valuenow, aria-valuetext
  • Live label updates via aria-live="polite"
  • Themeable through CSS custom properties

Source Code

View Source Code
ts
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

html
<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.

PreviewCode
RTL

External Scoring

Pass a normalized score from your own scoring engine. Set score to 04; the built-in heuristic is bypassed.

PreviewCode
RTL

Custom Level Labels

Override the default level strings (Weak, Fair, Good, Strong) by providing all five labels in order: empty, weak, fair, good, strong.

PreviewCode
RTL

Bar Only (No Visible Label)

Set show-label="false" to render only the visual segments while preserving the full meter semantics for screen readers.

PreviewCode
RTL

How the Built-in Scorer Works

The component uses a conservative heuristic based on length and character variety. The rules, from weakest to strongest:

ScoreLevelCondition
0emptyNo value
1weakLength < 6
2fairLength ≥ 8 with mixed case
3goodLength ≥ 8 with mixed case + digit or symbol
4strongLength ≥ 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

AttributeTypeDefaultDescription
valuestringPassword string to evaluate with the built-in heuristic
scorenumber-1External score override 0..4; -1 means use built-in scorer
show-labelbooleantrueShow visible textual level feedback below the bar
labelstringPassword strengthAccessible name (aria-label) on the meter element
labelsstring[]Built-in level namesAll five level labels: [empty, weak, fair, good, strong]

Parts

PartDescription
(none)No shadow parts are exposed

CSS Custom Properties

PropertyDefaultDescription
--password-strength-height0.375remSegment bar height
--password-strength-gapvar(--space-1)Gap between segments
--password-strength-radiusvar(--rounded-full)Segment corner radius
--password-strength-track-bgvar(--color-contrast-300)Inactive segment background color
--password-strength-track-bordervar(--color-contrast-400)Inactive segment border color
--password-strength-label-sizevar(--text-sm)Visible label font size
--password-strength-label-colorcurrentColorVisible label color
--password-strength-weak-colorvar(--color-warning-500)Active color for weak score
--password-strength-fair-colorvar(--color-warning-600)Active color for fair score
--password-strength-good-colorvar(--color-success-500)Active color for good score
--password-strength-strong-colorvar(--color-success-600)Active color for strong score

Accessibility

Meter Role
  • Uses role="meter" with aria-valuemin="0", aria-valuemax="4", and dynamic aria-valuenow.
  • Provides human-readable state through aria-valuetext (Weak, Fair, Good, Strong).
  • When score is 0 (empty), aria-valuetext is omitted to avoid announcing "empty".
Live Updates
  • The visible label uses aria-live="polite" and aria-atomic="true" to announce level transitions.
Reduced Motion
  • The shimmer transition respects prefers-reduced-motion: reduce.
Screen Readers
  • Decorative segments are hidden from the accessibility tree via aria-hidden="true".
  • Input — collect password values; forward input events to the meter.
  • Progress — generic determinate and indeterminate process tracking.
  • Form — compose validated authentication flows.