Password Field

Strength: too short

  • At least 12 characters
  • Upper- and lowercase letters
  • At least one number
  • At least one symbol
@use 'sprucecss/scss/spruce' as *;

.password-field {
  @include layout-stack('xs');

  &__control {
    position: relative;

    .form-control {
      padding-inline-end: 3rem;
    }
  }

  &__toggle {
    inset-block-start: 50%;
    inset-inline-end: spacer('xxs');
    position: absolute;
    translate: 0 -50%;

    &[aria-pressed='true'] {
      color: color('primary');
    }
  }

  &__strength {
    color: color('muted');
    font-size: config('font-size-sm', $typography);
  }

  &__rules {
    font-size: config('font-size-sm', $typography);
    list-style: none;
    padding: 0;
    @include layout-stack('xxs');

    li {
      align-items: center;
      color: color('muted');
      display: flex;
      gap: spacer('xs');

      &::before {
        block-size: 0.75rem;
        border: 2px solid currentcolor;
        border-radius: 50%;
        content: '';
        flex-shrink: 0;
        inline-size: 0.75rem;
      }

      &.is-valid {
        color: color('success', 'alert');

        &::before {
          background-color: currentcolor;
        }
      }
    }
  }
}
<div class="password-field" data-password-field>
    <label class="form-label form-label--required" for="new-password">New password</label>
    <div class="password-field__control">
        <input class="form-control" id="new-password" type="password" autocomplete="new-password" aria-describedby="password-strength password-rules" required>
        <button class="btn btn--ghost-secondary btn--icon btn--sm password-field__toggle" type="button" aria-pressed="false" aria-controls="new-password" data-password-toggle>
            <svg class="btn__icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M2 12s3.6-7 10-7 10 7 10 7-3.6 7-10 7S2 12 2 12z"/><circle cx="12" cy="12" r="3"/></svg>
            <span class="sr-only">Show password</span>
        </button>
    </div>
    <meter class="password-field__meter" min="0" max="4" low="2" high="3" optimum="4" value="0" aria-hidden="true" data-password-meter></meter>
    <p class="password-field__strength" id="password-strength" aria-live="polite" data-password-strength>Strength: too short</p>
    <ul class="password-field__rules" id="password-rules" role="list">
        <li data-rule="length">At least 12 characters</li>
        <li data-rule="case">Upper- and lowercase letters</li>
        <li data-rule="number">At least one number</li>
        <li data-rule="symbol">At least one symbol</li>
    </ul>
</div>
(() => {
  const rules = {
    length: (value) => value.length >= 12,
    case: (value) => /[a-z]/.test(value) && /[A-Z]/.test(value),
    number: (value) => /\d/.test(value),
    symbol: (value) => /[^A-Za-z0-9]/.test(value)
  };
  const labels = ['too weak', 'weak', 'fair', 'good', 'strong'];

  document.querySelectorAll('[data-password-field]').forEach((field) => {
    const input = field.querySelector('input');
    const toggle = field.querySelector('[data-password-toggle]');
    const meter = field.querySelector('[data-password-meter]');
    const strength = field.querySelector('[data-password-strength]');

    toggle.addEventListener('click', () => {
      const isVisible = toggle.getAttribute('aria-pressed') === 'true';

      input.type = isVisible ? 'password' : 'text';
      toggle.setAttribute('aria-pressed', String(!isVisible));
      toggle.querySelector('.sr-only').textContent = isVisible ? 'Show password' : 'Hide password';
    });

    input.addEventListener('input', () => {
      let score = 0;

      Object.entries(rules).forEach(([name, test]) => {
        const isValid = test(input.value);

        field.querySelector(`[data-rule="${name}"]`)?.classList.toggle('is-valid', isValid);
        score += isValid ? 1 : 0;
      });

      meter.value = score;
      strength.textContent = input.value.length ? `Strength: ${labels[score]}` : 'Strength: too short';
    });
  });
})();

Password Field

On this page

A password input with a show/hide toggle, a live strength meter and a checklist of the requirements.

Technical Details

  • The toggle is a real button with aria-pressed and a hidden label that switches between "Show password" and "Hide password"; it changes the input type between password and text.
  • The strength is shown with the native <meter> element (styled by Spruce's new progress generator) and announced through a polite live region.
  • The input is described by both the strength text and the rules (aria-describedby), and each rule gets an .is-valid class when met.
  • Use autocomplete="new-password" so password managers can suggest a strong password.

Documentation

Learn about Spruce CSS through our extensive documentation.

Components

Explore our UI library of accessible components built with Spruce CSS.

Blocks and Pages

Full-width page blocks and complete page templates, ready to copy.

Find us on GitHub

Did you find a bug? Have an idea or a question? Please open an issue to help us develop the project.