← Back
UI

Password Strength

Live password meter with animated strength bars and a per-rule validation checklist.

regex rules · animated strength bars · live feedback

PasswordStrength.tsx102 lines
"use client";

import { useState } from "react";

const LEVELS = [
  { label: "Too weak", color: "bg-red-500",    textColor: "text-red-500",    bars: 1 },
  { label: "Weak",     color: "bg-orange-500", textColor: "text-orange-500", bars: 2 },
  { label: "Fair",     color: "bg-yellow-500", textColor: "text-yellow-500", bars: 3 },
  { label: "Strong",   color: "bg-emerald-500",textColor: "text-emerald-500",bars: 4 },
];

function getScore(pwd: string): number {
  if (!pwd) return -1;
  let s = 0;
  if (pwd.length >= 8)          s++;
  if (/[A-Z]/.test(pwd))        s++;
  if (/[0-9]/.test(pwd))        s++;
  if (/[^A-Za-z0-9]/.test(pwd)) s++;
  return s - 1; // 0–3
}

const RULES = [
  { label: "At least 8 characters", test: (p: string) => p.length >= 8 },
  { label: "Uppercase letter",       test: (p: string) => /[A-Z]/.test(p) },
  { label: "Number",                 test: (p: string) => /[0-9]/.test(p) },
  { label: "Special character",      test: (p: string) => /[^A-Za-z0-9]/.test(p) },
];

export default function PasswordStrength() {
  const [pwd, setPwd] = useState("");
  const [show, setShow] = useState(false);
  const score = getScore(pwd);
  const level = score >= 0 ? LEVELS[score] : null;

  return (
    <div className="w-full max-w-sm mx-auto flex flex-col gap-5 py-10">
      {/* input */}
      <div className="relative">
        <input
          type={show ? "text" : "password"}
          value={pwd}
          onChange={(e) => setPwd(e.target.value)}
          placeholder="Enter a password"
          className="w-full px-4 py-3 pr-12 rounded-2xl border-2 border-zinc-200 dark:border-zinc-700 focus:border-violet-500 outline-none transition-colors text-sm bg-white dark:bg-zinc-900 text-zinc-900 dark:text-zinc-50 placeholder-zinc-300 dark:placeholder-zinc-600"
        />
        <button
          onClick={() => setShow((s) => !s)}
          className="absolute right-3 top-1/2 -translate-y-1/2 text-zinc-400 dark:text-zinc-600 hover:text-zinc-600 dark:hover:text-zinc-400 text-xs font-mono transition-colors"
        >
          {show ? "hide" : "show"}
        </button>
      </div>

      {/* strength bars */}
      <div className="flex gap-1.5">
        {LEVELS.map((l, i) => (
          <div
            key={l.label}
            className={`h-1.5 flex-1 rounded-full transition-all duration-300 ${
              level && i <= score ? l.color : "bg-zinc-200 dark:bg-zinc-800"
            }`}
          />
        ))}
      </div>

      {/* label */}
      {level && (
        <p className={`text-sm font-semibold -mt-2 transition-all ${level.textColor}`}>
          {level.label}
        </p>
      )}

      {/* rules checklist */}
      {pwd.length > 0 && (
        <div className="flex flex-col gap-2">
          {RULES.map((r) => {
            const ok = r.test(pwd);
            return (
              <div key={r.label} className="flex items-center gap-2">
                <span
                  className={`w-4 h-4 rounded-full flex items-center justify-center text-[10px] font-bold shrink-0 transition-all duration-200 ${
                    ok ? "bg-emerald-500 text-white" : "bg-zinc-200 dark:bg-zinc-800 text-zinc-400"
                  }`}
                >
                  {ok ? "✓" : ""}
                </span>
                <span className={`text-xs transition-colors ${ok ? "text-zinc-600 dark:text-zinc-400" : "text-zinc-400 dark:text-zinc-600"}`}>
                  {r.label}
                </span>
              </div>
            );
          })}
        </div>
      )}

      <p className="text-center text-[10px] font-mono text-zinc-400 dark:text-zinc-600">
        regex rules · animated strength bars · live feedback
      </p>
    </div>
  );
}

More in UI