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