🔥 Oxide UI v0.1.0 — Dark mode only, copy-paste ready. Get started →
Skip to content

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-label that 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

Released under the MIT License.