Forms forminputpasswordsecurityvalidation

Password Strength Input

Interactive password input with dynamic real-time entropy calculation, strength meter bar, and visibility toggle.

Preview

Too weak

Usage

Copy the full block below to use this component with its imports.

astro
---
import { PasswordStrengthInput } from 'astro-component-kit';
---

<PasswordStrengthInput id="user-password" label="Create Password" minLength={8} />
--- import { PasswordStrengthInput } from 'astro-component-kit'; --- <PasswordStrengthInput id="user-password" label="Create Password" minLength={8} />

Manual Installation

If you are not using the npm package, create a new file src/components/lib/PasswordStrengthInput.astro and paste the following code:

astro
---
/**
 * PasswordStrengthInput — Secure password input field with visual strength estimator bar and eye toggle.
 * 
 * @param {string} id - HTML ID for the password input.
 * @param {string} label - Input label text. Default is 'Password'.
 * @param {string} name - Form field name. Default is 'password'.
 * @param {string} placeholder - Placeholder text. Default is 'Enter secure password'.
 * @param {boolean} required - HTML required field attribute.
 */
interface Props {
  id: string;
  label?: string;
  name?: string;
  placeholder?: string;
  required?: boolean;
}

const {
  id,
  label = 'Password',
  name = 'password',
  placeholder = 'Enter secure password',
  required = false,
} = Astro.props;
---

<div class="pwd-group" data-pwd-group>
  <div class="pwd-group__header">
    <label for={id} class="pwd-group__label">{label}</label>
    <span class="pwd-group__status" data-pwd-status>Too weak</span>
  </div>

  <div class="pwd-group__field">
    <input
      type="password"
      id={id}
      name={name}
      placeholder={placeholder}
      required={required}
      class="pwd-group__input"
      data-pwd-input
      autocomplete="new-password"
    />
    <button
      type="button"
      class="pwd-group__toggle"
      aria-label="Toggle password visibility"
      data-pwd-toggle
    >
      <svg class="eye-open" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
        <path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z" />
        <circle cx="12" cy="12" r="3" />
      </svg>
      <svg class="eye-closed" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:none;">
        <path d="M9.88 9.88a3 3 0 1 0 4.24 4.24" />
        <path d="M10.73 5.08A10.43 10.43 0 0 1 12 5c7 0 10 7 10 7a13.16 13.16 0 0 1-1.67 2.68" />
        <path d="M6.61 6.61A13.526 13.526 0 0 0 2 12s3 7 10 7a9.74 9.74 0 0 0 5.39-1.61" />
        <line x1="2" x2="22" y1="2" y2="22" />
      </svg>
    </button>
  </div>

  <!-- 4-segment strength meter -->
  <div class="pwd-group__meter" aria-hidden="true">
    <div class="pwd-group__segment" data-seg="1"></div>
    <div class="pwd-group__segment" data-seg="2"></div>
    <div class="pwd-group__segment" data-seg="3"></div>
    <div class="pwd-group__segment" data-seg="4"></div>
  </div>
</div>

<style>
  .pwd-group {
    display: flex;
    flex-direction: column;
    gap: var(--sp-2, 0.5rem);
    width: 100%;
    max-width: 360px;
    font-family: var(--font-sans, system-ui, sans-serif);
  }

  .pwd-group__header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 0 0.2rem;
  }

  .pwd-group__label {
    font-size: 0.85rem;
    font-weight: 600;
    color: var(--c-text-1, #ffffff);
  }

  .pwd-group__status {
    font-size: 0.72rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: #ef4444;
    transition: color 0.3s ease;
  }

  .pwd-group__field {
    position: relative;
    width: 100%;
  }

  .pwd-group__input {
    width: 100%;
    padding: 0.75rem 2.8rem 0.75rem 1rem;
    background: rgba(255, 255, 255, 0.03);
    backdrop-filter: blur(12px);
    border: 1px solid rgba(255, 255, 255, 0.1);
    border-radius: var(--r-md, 12px);
    color: var(--c-text-1, #ffffff);
    font-size: 0.92rem;
    outline: none;
    transition: border-color 0.2s ease, box-shadow 0.2s ease;
  }

  .pwd-group__input:focus {
    border-color: rgba(184, 134, 11, 0.5);
    box-shadow: 0 0 0 3px rgba(184, 134, 11, 0.15);
  }

  .pwd-group__input::placeholder {
    color: rgba(255, 255, 255, 0.3);
  }

  .pwd-group__toggle {
    position: absolute;
    right: 0.75rem;
    top: 50%;
    transform: translateY(-50%);
    background: transparent;
    border: none;
    color: var(--c-text-3, #737373);
    cursor: pointer;
    padding: 0.3rem;
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: var(--r-sm, 6px);
    transition: color 0.2s ease;
  }

  .pwd-group__toggle:hover {
    color: var(--c-text-1, #ffffff);
  }

  .pwd-group__meter {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: 6px;
    height: 4px;
    margin-top: 4px;
  }

  .pwd-group__segment {
    height: 100%;
    border-radius: 4px;
    background: rgba(255, 255, 255, 0.08);
    transition: background 0.3s cubic-bezier(0.16, 1, 0.3, 1);
  }

  /* Dynamic levels */
  .pwd-group[data-level="1"] .pwd-group__segment[data-seg="1"] {
    background: #ef4444;
  }
  .pwd-group[data-level="1"] .pwd-group__status {
    color: #ef4444;
  }

  .pwd-group[data-level="2"] .pwd-group__segment[data-seg="1"],
  .pwd-group[data-level="2"] .pwd-group__segment[data-seg="2"] {
    background: #f59e0b;
  }
  .pwd-group[data-level="2"] .pwd-group__status {
    color: #f59e0b;
  }

  .pwd-group[data-level="3"] .pwd-group__segment[data-seg="1"],
  .pwd-group[data-level="3"] .pwd-group__segment[data-seg="2"],
  .pwd-group[data-level="3"] .pwd-group__segment[data-seg="3"] {
    background: #3b82f6;
  }
  .pwd-group[data-level="3"] .pwd-group__status {
    color: #3b82f6;
  }

  .pwd-group[data-level="4"] .pwd-group__segment {
    background: #10b981;
    box-shadow: 0 0 8px rgba(16, 185, 129, 0.4);
  }
  .pwd-group[data-level="4"] .pwd-group__status {
    color: #10b981;
  }
</style>

<script>
  function initPasswordGroups() {
    document.querySelectorAll('[data-pwd-group]').forEach((group) => {
      const input = group.querySelector('[data-pwd-input]') as HTMLInputElement;
      const toggle = group.querySelector('[data-pwd-toggle]') as HTMLButtonElement;
      const status = group.querySelector('[data-pwd-status]') as HTMLElement;
      if (!input || !toggle) return;

      const eyeOpen = toggle.querySelector('.eye-open') as HTMLElement;
      const eyeClosed = toggle.querySelector('.eye-closed') as HTMLElement;

      toggle.addEventListener('click', () => {
        const isPassword = input.type === 'password';
        input.type = isPassword ? 'text' : 'password';
        if (eyeOpen && eyeClosed) {
          eyeOpen.style.display = isPassword ? 'none' : 'block';
          eyeClosed.style.display = isPassword ? 'block' : 'none';
        }
      });

      input.addEventListener('input', () => {
        const val = input.value;
        let score = 0;
        if (val.length >= 6) score++;
        if (val.length >= 10) score++;
        if (/[A-Z]/.test(val) && /[0-9]/.test(val)) score++;
        if (/[^A-Za-z0-9]/.test(val)) score++;

        group.setAttribute('data-level', String(score));

        const labels = ['Too weak', 'Weak', 'Fair', 'Strong', 'Secure & Robust'];
        if (status) {
          status.textContent = labels[score] || 'Too weak';
        }
      });
    });
  }

  initPasswordGroups();
  document.addEventListener('astro:page-load', initPasswordGroups);
</script>
--- /** * PasswordStrengthInput — Secure password input field with visual strength estimator bar and eye toggle. * * @param {string} id - HTML ID for the password input. * @param {string} label - Input label text. Default is 'Password'. * @param {string} name - Form field name. Default is 'password'. * @param {string} placeholder - Placeholder text. Default is 'Enter secure password'. * @param {boolean} required - HTML required field attribute. */ interface Props { id: string; label?: string; name?: string; placeholder?: string; required?: boolean; } const { id, label = 'Password', name = 'password', placeholder = 'Enter secure password', required = false, } = Astro.props; --- <div class="pwd-group" data-pwd-group> <div class="pwd-group__header"> <label for={id} class="pwd-group__label">{label}</label> <span class="pwd-group__status" data-pwd-status>Too weak</span> </div> <div class="pwd-group__field"> <input type="password" id={id} name={name} placeholder={placeholder} required={required} class="pwd-group__input" data-pwd-input autocomplete="new-password" /> <button type="button" class="pwd-group__toggle" aria-label="Toggle password visibility" data-pwd-toggle > <svg class="eye-open" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z" /> <circle cx="12" cy="12" r="3" /> </svg> <svg class="eye-closed" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:none;"> <path d="M9.88 9.88a3 3 0 1 0 4.24 4.24" /> <path d="M10.73 5.08A10.43 10.43 0 0 1 12 5c7 0 10 7 10 7a13.16 13.16 0 0 1-1.67 2.68" /> <path d="M6.61 6.61A13.526 13.526 0 0 0 2 12s3 7 10 7a9.74 9.74 0 0 0 5.39-1.61" /> <line x1="2" x2="22" y1="2" y2="22" /> </svg> </button> </div> <!-- 4-segment strength meter --> <div class="pwd-group__meter" aria-hidden="true"> <div class="pwd-group__segment" data-seg="1"></div> <div class="pwd-group__segment" data-seg="2"></div> <div class="pwd-group__segment" data-seg="3"></div> <div class="pwd-group__segment" data-seg="4"></div> </div> </div> <style> .pwd-group { display: flex; flex-direction: column; gap: var(--sp-2, 0.5rem); width: 100%; max-width: 360px; font-family: var(--font-sans, system-ui, sans-serif); } .pwd-group__header { display: flex; justify-content: space-between; align-items: center; padding: 0 0.2rem; } .pwd-group__label { font-size: 0.85rem; font-weight: 600; color: var(--c-text-1, #ffffff); } .pwd-group__status { font-size: 0.72rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; color: #ef4444; transition: color 0.3s ease; } .pwd-group__field { position: relative; width: 100%; } .pwd-group__input { width: 100%; padding: 0.75rem 2.8rem 0.75rem 1rem; background: rgba(255, 255, 255, 0.03); backdrop-filter: blur(12px); border: 1px solid rgba(255, 255, 255, 0.1); border-radius: var(--r-md, 12px); color: var(--c-text-1, #ffffff); font-size: 0.92rem; outline: none; transition: border-color 0.2s ease, box-shadow 0.2s ease; } .pwd-group__input:focus { border-color: rgba(184, 134, 11, 0.5); box-shadow: 0 0 0 3px rgba(184, 134, 11, 0.15); } .pwd-group__input::placeholder { color: rgba(255, 255, 255, 0.3); } .pwd-group__toggle { position: absolute; right: 0.75rem; top: 50%; transform: translateY(-50%); background: transparent; border: none; color: var(--c-text-3, #737373); cursor: pointer; padding: 0.3rem; display: flex; align-items: center; justify-content: center; border-radius: var(--r-sm, 6px); transition: color 0.2s ease; } .pwd-group__toggle:hover { color: var(--c-text-1, #ffffff); } .pwd-group__meter { display: grid; grid-template-columns: repeat(4, 1fr); gap: 6px; height: 4px; margin-top: 4px; } .pwd-group__segment { height: 100%; border-radius: 4px; background: rgba(255, 255, 255, 0.08); transition: background 0.3s cubic-bezier(0.16, 1, 0.3, 1); } /* Dynamic levels */ .pwd-group[data-level="1"] .pwd-group__segment[data-seg="1"] { background: #ef4444; } .pwd-group[data-level="1"] .pwd-group__status { color: #ef4444; } .pwd-group[data-level="2"] .pwd-group__segment[data-seg="1"], .pwd-group[data-level="2"] .pwd-group__segment[data-seg="2"] { background: #f59e0b; } .pwd-group[data-level="2"] .pwd-group__status { color: #f59e0b; } .pwd-group[data-level="3"] .pwd-group__segment[data-seg="1"], .pwd-group[data-level="3"] .pwd-group__segment[data-seg="2"], .pwd-group[data-level="3"] .pwd-group__segment[data-seg="3"] { background: #3b82f6; } .pwd-group[data-level="3"] .pwd-group__status { color: #3b82f6; } .pwd-group[data-level="4"] .pwd-group__segment { background: #10b981; box-shadow: 0 0 8px rgba(16, 185, 129, 0.4); } .pwd-group[data-level="4"] .pwd-group__status { color: #10b981; } </style> <script> function initPasswordGroups() { document.querySelectorAll('[data-pwd-group]').forEach((group) => { const input = group.querySelector('[data-pwd-input]') as HTMLInputElement; const toggle = group.querySelector('[data-pwd-toggle]') as HTMLButtonElement; const status = group.querySelector('[data-pwd-status]') as HTMLElement; if (!input || !toggle) return; const eyeOpen = toggle.querySelector('.eye-open') as HTMLElement; const eyeClosed = toggle.querySelector('.eye-closed') as HTMLElement; toggle.addEventListener('click', () => { const isPassword = input.type === 'password'; input.type = isPassword ? 'text' : 'password'; if (eyeOpen && eyeClosed) { eyeOpen.style.display = isPassword ? 'none' : 'block'; eyeClosed.style.display = isPassword ? 'block' : 'none'; } }); input.addEventListener('input', () => { const val = input.value; let score = 0; if (val.length >= 6) score++; if (val.length >= 10) score++; if (/[A-Z]/.test(val) && /[0-9]/.test(val)) score++; if (/[^A-Za-z0-9]/.test(val)) score++; group.setAttribute('data-level', String(score)); const labels = ['Too weak', 'Weak', 'Fair', 'Strong', 'Secure & Robust']; if (status) { status.textContent = labels[score] || 'Too weak'; } }); }); } initPasswordGroups(); document.addEventListener('astro:page-load', initPasswordGroups); </script>

Quick Info

Category
Forms
Filename
PasswordStrengthInput.astro
Dependencies
None — pure Astro + CSS
Tags
forminputpasswordsecurityvalidation