# Password Input

> Password field with show / hide toggle, strength meter and rules checklist.

- Author: Atelier UI
- License: MIT
- Page: https://chromotion.dev/en/components/password-input
- Site: Atelier UI (https://chromotion.dev)

## Install

```bash
npx shadcn@latest add https://chromotion.dev/r/password-input.json
```

Stack: React + TypeScript + Tailwind CSS v4, shadcn/ui conventions. `cn` comes from `@/lib/utils`.

## Dependencies

```bash
npm install lucide-react
```

## Usage

```tsx title="components/password-input-demo.tsx"
import { PasswordInput } from "@/components/password-input"

export default function PasswordInputDemo() {
  return <PasswordInput label="Create a password" placeholder="Enter a password" defaultValue="Atelier2" />
}
```

## Source

```tsx title="components/password-input.tsx"
"use client"

import * as React from "react"
import { Check, Eye, EyeOff, X } from "lucide-react"

import { cn } from "@/lib/utils"

export type PasswordRule = { label: string; test: (value: string) => boolean }

export const defaultPasswordRules: PasswordRule[] = [
  { label: "At least 8 characters", test: (v) => v.length >= 8 },
  { label: "An uppercase letter", test: (v) => /[A-Z]/.test(v) },
  { label: "A number", test: (v) => /\d/.test(v) },
  { label: "A symbol", test: (v) => /[^A-Za-z0-9]/.test(v) },
]

const levels = [
  { label: "Too weak", color: "bg-red-500" },
  { label: "Weak", color: "bg-orange-500" },
  { label: "Fair", color: "bg-amber-500" },
  { label: "Good", color: "bg-lime-500" },
  { label: "Strong", color: "bg-emerald-500" },
]

export interface PasswordInputProps extends Omit<React.ComponentProps<"input">, "type"> {
  label?: string
  rules?: PasswordRule[]
  /** Strength bar under the field */
  showStrength?: boolean
  /** Checklist of the rules */
  showRules?: boolean
}

export function PasswordInput({
  label = "Password",
  rules = defaultPasswordRules,
  showStrength = true,
  showRules = true,
  value,
  defaultValue,
  onChange,
  className,
  id,
  ...props
}: PasswordInputProps) {
  const autoId = React.useId()
  const inputId = id ?? autoId
  const [internal, setInternal] = React.useState(String(defaultValue ?? ""))
  const [visible, setVisible] = React.useState(false)
  const current = value !== undefined ? String(value) : internal
  const passed = rules.filter((rule) => rule.test(current)).length
  const score = current ? Math.round((passed / rules.length) * (levels.length - 1)) : 0
  const level = levels[score]

  return (
    <div className={cn("flex w-full max-w-sm flex-col gap-2", className)}>
      <label htmlFor={inputId} className="text-sm font-medium">
        {label}
      </label>
      <div className="flex h-10 items-center rounded-lg border bg-background pr-1 shadow-xs transition-[border-color,box-shadow] focus-within:border-ring focus-within:ring-3 focus-within:ring-ring/20">
        <input
          id={inputId}
          type={visible ? "text" : "password"}
          value={current}
          onChange={(event) => {
            setInternal(event.target.value)
            onChange?.(event)
          }}
          aria-describedby={`${inputId}-strength`}
          className="h-full min-w-0 flex-1 bg-transparent px-3 text-sm outline-none placeholder:text-muted-foreground"
          {...props}
        />
        <button
          type="button"
          onClick={() => setVisible((v) => !v)}
          aria-label={visible ? "Hide password" : "Show password"}
          aria-pressed={visible}
          className="flex size-8 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none"
        >
          {visible ? <EyeOff className="size-4" /> : <Eye className="size-4" />}
        </button>
      </div>
      <div id={`${inputId}-strength`} className="flex flex-col gap-2">
        {showStrength && (
          <div className="flex items-center gap-3">
            <div className="flex flex-1 gap-1">
              {levels.slice(1).map((step, i) => (
                <span
                  key={step.label}
                  className={cn(
                    "h-1 flex-1 rounded-full transition-colors duration-300",
                    current && i < score ? level.color : "bg-muted"
                  )}
                />
              ))}
            </div>
            <span className="w-16 text-right text-xs text-muted-foreground" aria-live="polite">
              {current ? level.label : ""}
            </span>
          </div>
        )}
        {showRules && (
          <ul className="grid gap-1 text-xs sm:grid-cols-2">
            {rules.map((rule) => {
              const ok = rule.test(current)
              return (
                <li
                  key={rule.label}
                  className={cn("flex items-center gap-1.5 transition-colors", ok ? "text-emerald-600 dark:text-emerald-400" : "text-muted-foreground")}
                >
                  {ok ? <Check className="size-3.5" /> : <X className="size-3.5" />}
                  {rule.label}
                </li>
              )
            })}
          </ul>
        )}
      </div>
    </div>
  )
}
```
