Hazine

Password Input

A password field with visibility toggle and live strength meter.

Installation

npx shadcn@latest add @hazine/password-input

Source

"use client"

import * as React from "react"
import { EyeIcon, EyeOffIcon } from "lucide-react"

import { cn } from "@/lib/utils"
import { Input } from "@/components/ui/input"

function strength(value: string) {
  let score = 0
  if (value.length >= 8) score++
  if (value.length >= 12) score++
  if (/[A-Z]/.test(value) && /[a-z]/.test(value)) score++
  if (/\d/.test(value)) score++
  if (/[^A-Za-z0-9]/.test(value)) score++
  return Math.min(score, 4)
}

const labels = ["Too weak", "Weak", "Fair", "Good", "Strong"]
const colors = [
  "bg-red-500",
  "bg-red-500",
  "bg-amber-500",
  "bg-emerald-500",
  "bg-emerald-500",
]

interface PasswordInputProps
  extends Omit<React.ComponentProps<typeof Input>, "type"> {
  showStrength?: boolean
}

export function PasswordInput({
  showStrength = false,
  className,
  onChange,
  ...props
}: PasswordInputProps) {
  const [visible, setVisible] = React.useState(false)
  const [value, setValue] = React.useState("")
  const score = strength(value)

  return (
    <div className="flex w-full flex-col gap-2">
      <div className="relative">
        <Input
          type={visible ? "text" : "password"}
          className={cn("pr-9", className)}
          onChange={(event) => {
            setValue(event.target.value)
            onChange?.(event)
          }}
          {...props}
        />
        <button
          type="button"
          aria-label={visible ? "Hide password" : "Show password"}
          className="absolute inset-y-0 right-0 flex w-9 items-center justify-center text-muted-foreground hover:text-foreground"
          onClick={() => setVisible((v) => !v)}
        >
          {visible ? (
            <EyeOffIcon className="size-4" />
          ) : (
            <EyeIcon className="size-4" />
          )}
        </button>
      </div>
      {showStrength && value.length > 0 && (
        <div className="flex flex-col gap-1.5">
          <div className="flex gap-1" aria-hidden="true">
            {Array.from({ length: 4 }).map((_, i) => (
              <div
                key={i}
                className={cn(
                  "h-1 flex-1 rounded-full bg-muted transition-colors",
                  i < score && colors[score]
                )}
              />
            ))}
          </div>
          <p className="text-xs text-muted-foreground" role="status">
            {labels[score]}
          </p>
        </div>
      )}
    </div>
  )
}

Dependencies