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 score externally (for example with zxcvbn) and pass it through the score prop.

Features

  • Real-time strength feedback from password input
  • 4-segment progress bar with semantic levels
  • Built-in heuristic scoring (length + character variety)
  • Optional external score override (0..4)
  • Accessible meter semantics (role="meter", aria-valuenow, aria-valuetext)
  • Live label updates for assistive technologies
  • Themeable through CSS custom properties

Source Code

View Source Code
ts
import { computed, define, html } from '@vielzeug/craftit';
import { prop } from '@vielzeug/craftit';

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 <bit-password-strength>. */
export type BitPasswordStrengthProps = {
  /** 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 buildit 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 bit-password-strength
 *
 * @attr {string} value       Password to evaluate
 * @attr {number} score       Optional score override (0..4)
 * @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
 * <bit-password-strength></bit-password-strength>
 * ```
 */
export const PASSWORD_STRENGTH_TAG = define<BitPasswordStrengthProps>('bit-password-strength', {
  props: {
    label: 'Password strength',
    labels: undefined,
    score: prop.number(-1),
    'show-label': true,
    value: prop.string(''),
  },

  setup(props, { host }) {
    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
    host.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
<bit-password-strength value="Tr0ub4dor&3"></bit-password-strength>

<script type="module">
  import '@vielzeug/buildit/password-strength';
</script>

Common Registration Flow

Bind the password meter to your password input in input events.

PreviewCode
RTL

External Scoring Integration

If your backend or client uses a dedicated scoring engine, pass normalized score directly.

PreviewCode
RTL

Hide Visible Label

Set the property show-label to false if you only want the visual bar while preserving meter semantics.

PreviewCode
RTL

API Reference

Attributes

AttributeTypeDefaultDescription
valuestring''Password string to evaluate
scorenumbercomputed from valueOptional external score override (0..4)
show-labelbooleantrueShow visible textual level feedback
labelstringPassword strengthAccessible name for assistive technologies

CSS Custom Properties

PropertyDefaultDescription
--password-strength-height0.375remSegment bar height
--password-strength-gap0.25remGap between segments
--password-strength-radiusvar(--rounded-full)Segment corner radius
--password-strength-track-bgvar(--color-contrast-300)Inactive segment 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

  • 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).
  • Uses aria-live="polite" on visible label to announce level transitions.
  • Decorative segments are hidden from screen readers via aria-hidden="true".
  • Input for collecting password values.
  • Progress for generic determinate and indeterminate process tracking.
  • Form for composing validated authentication flows.