Strong Password
<input type="password"> + computed score
A password field with visibility toggle and a live strength meter driven by requirement checks. Tells users exactly what's missing rather than just showing a colour.
Overview
Preview
Input with toggle
html
<div class="relative">
<input
:type="show ? 'text' : 'password'"
v-model="password"
placeholder="Enter a strong password"
class="block w-full rounded-md border border-zinc-700 bg-zinc-900 py-2.5 pl-3 pr-10
text-sm text-zinc-100 placeholder-zinc-600 outline-none transition-all
focus:border-[#D40C37] focus:ring-2 focus:ring-[#D40C37]/15"
/>
<button
@click="show = !show"
type="button"
:aria-label="show ? 'Hide password' : 'Show password'"
class="absolute inset-y-0 right-0 flex items-center px-3 text-zinc-500 hover:text-zinc-300 transition-colors"
>
<!-- Eye / eye-off icon -->
</button>
</div>Strength meter
vue
<script setup>
import { ref, computed } from 'vue'
const password = ref('')
const show = ref(false)
const requirements = computed(() => [
{ label: 'At least 8 characters', met: password.value.length >= 8 },
{ label: 'One uppercase letter (A–Z)', met: /[A-Z]/.test(password.value) },
{ label: 'One number (0–9)', met: /[0-9]/.test(password.value) },
{ label: 'One special character (!@#...)', met: /[^A-Za-z0-9]/.test(password.value) },
])
const score = computed(() => requirements.value.filter(r => r.met).length)
const strengthLabel = computed(() => ['', 'Weak', 'Fair', 'Good', 'Strong'][score.value])
const strengthColor = computed(() => ({
1: 'bg-red-500',
2: 'bg-amber-500',
3: 'bg-yellow-400',
4: 'bg-emerald-500',
})[score.value] ?? '')
const strengthTextColor = computed(() => ({
1: 'text-red-400',
2: 'text-amber-400',
3: 'text-yellow-400',
4: 'text-emerald-400',
})[score.value] ?? '')
</script>
<template>
<!-- Strength bar (4 segments) -->
<div v-if="password.length" class="mt-1.5 space-y-1.5">
<div class="flex gap-1">
<div
v-for="n in 4" :key="n"
class="h-1 flex-1 rounded-full transition-all duration-300"
:class="n <= score ? strengthColor : 'bg-zinc-800'"
></div>
</div>
<div class="flex justify-between items-center">
<p class="text-xs font-medium" :class="strengthTextColor">{{ strengthLabel }}</p>
<p class="text-[10px] text-zinc-600">{{ password.length }} chars</p>
</div>
</div>
<!-- Per-requirement checklist -->
<ul v-if="password.length" class="mt-2 space-y-1">
<li v-for="req in requirements" :key="req.label"
class="flex items-center gap-2 text-xs"
:class="req.met ? 'text-emerald-400' : 'text-zinc-600'"
>
<svg v-if="req.met" class="h-3 w-3 shrink-0" ...>✓</svg>
<svg v-else class="h-3 w-3 shrink-0 text-zinc-700" ...>○</svg>
{{ req.label }}
</li>
</ul>
</template>Custom requirements
Swap in any requirement set:
vue
const requirements = computed(() => [
{ label: 'At least 12 characters', met: password.value.length >= 12 },
{ label: 'No spaces', met: !/\s/.test(password.value) },
{ label: 'Matches confirm password', met: password.value === confirm.value && confirm.value.length > 0 },
])Accessibility notes
- Use
type="password"by default — don't expose passwords without the user's explicit action - The show/hide button needs
aria-labelthat reflects the current state:"Show password"/"Hide password" - The strength label and requirements list should update live — wrap in
aria-live="polite"so screen readers announce changes as the user types - Never block paste in password fields — it prevents users from using password managers