I'm working on new components <3
HextaUIHextaUI
Application

Password Strength Meter

Visual indicator for password strength with validation criteria.

Preview

At least 8 characters
Contains uppercase letter
Contains number
Contains special character

Code

PasswordStrengthMeter.tsx
"use client";
 
import { useEffect, useState, useMemo } from "react";
import { cn } from "@/lib/utils";
 
export interface PasswordStrengthMeterProps {
  password: string;
  minLength?: number;
  className?: string;
  barClassName?: string;
  criteriaClassName?: string;
  colors?: string[];
  levels?: number;
  customRequirements?: { label: string; test: (pass: string) => boolean }[];
}
 
export function PasswordStrengthMeter({
  password,
  minLength = 8,
  className,
  barClassName,
  criteriaClassName,
  colors = ["#dc2626", "#ea580c", "#16a34a", "#15803d"],
  levels = 4,
  customRequirements,
}: PasswordStrengthMeterProps) {
  const [strength, setStrength] = useState<number>(0);
  const [requirementsMet, setRequirementsMet] = useState<boolean[]>([]);
 
  const defaultRequirements = useMemo(
    () => [
      {
        label: `At least ${minLength} characters`,
        test: (pass: string) => pass?.length >= minLength,
      },
      {
        label: "Contains uppercase letter",
        test: (pass: string) => /[A-Z]/.test(pass),
      },
      {
        label: "Contains number",
        test: (pass: string) => /[0-9]/.test(pass),
      },
      {
        label: "Contains special character",
        test: (pass: string) => /[^A-Za-z0-9]/.test(pass),
      },
    ],
    [minLength],
  );
 
  const requirements = customRequirements || defaultRequirements;
 
  useEffect(() => {
    const met = requirements.map((req) => req.test(password));
    setRequirementsMet(met);
    const metCount = met.filter(Boolean).length;
    const newStrength = Math.min(
      Math.floor((metCount / requirements.length) * levels),
      levels,
    );
    setStrength(newStrength);
  }, [password, requirements, levels]);
 
  return (
    <div
      className={cn("space-y-3", className)}
      role="region"
      aria-label="Password strength meter"
    >
      <div className={cn("flex gap-1", barClassName)}>
        {[...Array(levels)].map((_, i) => (
          <div
            key={i}
            className="h-2 flex-1 rounded-full bg-muted transition-all"
            style={{
              backgroundColor: i < strength ? colors[strength - 1] : "",
            }}
            aria-hidden="true"
          />
        ))}
      </div>
      <div
        className={cn("text-sm text-muted-foreground pt-2", criteriaClassName)}
      >
        {requirements.map((req, i) => (
          <div
            key={req.label}
            className={cn(
              "flex items-center gap-2",
              requirementsMet[i] && "text-green-600",
            )}
          >
            <span className="text-xs">•</span>
            {req.label}
          </div>
        ))}
      </div>
    </div>
  );
}

Usage

App.tsx
import { PasswordStrengthMeter } from "@/components/library/application/PasswordStrengthMeter";
 
export const PasswordStrengthMeter = () => {
  const [password, setPassword] = useState("");
 
  return (
    <div className="flex flex-col gap-4">
      <input
        type="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
        className="w-full px-3 py-2 border rounded-md focus-visible:ring-0 focus-within:ring-0 focus:outline-white/10"
        placeholder="Enter your password"
      />
      <PasswordStrengthMeter password={password} />
    </div>
  );
};
Edit on GitHub

Last updated on

On this page