UIPackage
Menu

Password Input

password-input ui
Edit on GitHub

Password input with show/hide toggle (Eye/EyeOff) and optional strength meter (weak/fair/good/strong with colored bar). Supports min length display, disabled, placeholder, and size variants.

Also available for Vue ->

Installation

$ npx shadcn@latest add https://uipkge.dev/r/react/password-input.json
Named registry: npx shadcn@latest add @uipkge-react/password-input Installs to: components/ui/password-input/

Examples

Props

Name Type / Values Default Required
size
'sm''default''lg'
default optional
variant
'outlined''filled''borderless'
outlined optional
defaultValue string optional
showStrength boolean optional
showToggle boolean optional
minLength number optional
className

Wrapper className. Falls through to the bordered control, not the <input>.

string optional

Schema

Type aliases from this item's source — use them to shape the data you pass in.

StrengthResult
interface StrengthResult {
  score: number
  label: 'weak' | 'fair' | 'good' | 'strong'
  color: string
  barColor: string
  percent: number
}

Files installed (3)

  • components/ui/password-input/PasswordInput.tsx 6.5 kB
    'use client'
    
    import * as React from 'react'
    import { Eye, EyeOff } from 'lucide-react'
    import { cn } from '@/lib/utils'
    import { passwordInputVariants, type PasswordInputVariants } from './password-input.variants'
    
    export interface PasswordInputProps
      extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'size' | 'prefix'>,
        PasswordInputVariants {
      defaultValue?: string
      showStrength?: boolean
      showToggle?: boolean
      minLength?: number
      /** Wrapper className. Falls through to the bordered control, not the <input>. */
      className?: string
    }
    
    interface StrengthResult {
      score: number
      label: 'weak' | 'fair' | 'good' | 'strong'
      color: string
      barColor: string
      percent: number
    }
    
    function computeStrength(pwd: string): StrengthResult {
      if (!pwd) return { score: 0, label: 'weak', color: '', barColor: 'bg-transparent', percent: 0 }
    
      let score = 0
      if (pwd.length >= 6) score++
      if (pwd.length >= 10) score++
      if (/[A-Z]/.test(pwd) && /[a-z]/.test(pwd)) score++
      if (/\d/.test(pwd)) score++
      if (/[^A-Za-z0-9]/.test(pwd)) score++
    
      if (score <= 1) {
        return { score, label: 'weak', color: 'text-destructive', barColor: 'bg-destructive', percent: 25 }
      }
      if (score <= 2) {
        return {
          score,
          label: 'fair',
          color: 'text-[var(--warning)]',
          barColor: 'bg-[var(--warning)]',
          percent: 50,
        }
      }
      if (score <= 3) {
        return {
          score,
          label: 'good',
          color: 'text-[var(--info)]',
          barColor: 'bg-[var(--info)]',
          percent: 75,
        }
      }
      return {
        score,
        label: 'strong',
        color: 'text-[var(--success)]',
        barColor: 'bg-[var(--success)]',
        percent: 100,
      }
    }
    
    const PasswordInput = React.forwardRef<HTMLInputElement, PasswordInputProps>(
      (
        {
          value,
          defaultValue,
          placeholder = 'Enter password',
          size = 'default',
          variant = 'outlined',
          disabled = false,
          readOnly = false,
          showStrength = false,
          showToggle = true,
          minLength = 0,
          maxLength,
          id,
          name,
          autoComplete = 'current-password',
          className,
          onChange,
          onFocus,
          onBlur,
          ...rest
        },
        ref,
      ) => {
        const innerRef = React.useRef<HTMLInputElement | null>(null)
        const setRefs = React.useCallback(
          (node: HTMLInputElement | null) => {
            innerRef.current = node
            if (typeof ref === 'function') ref(node)
            else if (ref) (ref as React.MutableRefObject<HTMLInputElement | null>).current = node
          },
          [ref],
        )
    
        const isControlled = value !== undefined
        const [internal, setInternal] = React.useState<string>(
          defaultValue != null ? String(defaultValue) : '',
        )
        const currentValue = isControlled ? String(value ?? '') : internal
    
        const [passwordVisible, setPasswordVisible] = React.useState(false)
        const computedType = passwordVisible ? 'text' : 'password'
    
        const strength = React.useMemo<StrengthResult>(() => computeStrength(currentValue), [currentValue])
        const meetsMinLength = currentValue.length >= minLength
    
        function toggleVisibility() {
          if (disabled || readOnly) return
          setPasswordVisible((v) => !v)
          innerRef.current?.focus()
        }
    
        function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
          if (!isControlled) setInternal(e.target.value)
          onChange?.(e)
        }
    
        const wrapperClasses = cn(
          passwordInputVariants({ size, variant }),
          'focus-within:border-ring focus-within:ring-ring/50 focus-within:ring-[3px]',
          disabled && 'pointer-events-none opacity-50 cursor-not-allowed bg-muted/30',
          className,
        )
    
        const inputPadding = size === 'sm' ? 'px-2.5' : size === 'lg' ? 'px-4' : 'px-3'
        const togglePadding = size === 'sm' ? 'pr-2' : size === 'lg' ? 'pr-3' : 'pr-2.5'
    
        return (
          <div className="flex w-full flex-col gap-2">
            <div className={wrapperClasses} data-uipkge="" data-slot="password-input" data-size={size} data-variant={variant}>
              <input
                id={id}
                ref={setRefs}
                value={currentValue}
                type={computedType}
                disabled={disabled}
                readOnly={readOnly}
                maxLength={maxLength}
                placeholder={placeholder}
                name={name}
                autoComplete={autoComplete}
                onChange={handleChange}
                onFocus={onFocus}
                onBlur={onBlur}
                className={cn('placeholder:text-muted-foreground w-full min-w-0 flex-1 bg-transparent outline-none', inputPadding)}
                {...rest}
              />
              {showToggle && (
                <div className={cn('flex shrink-0 items-center', togglePadding)}>
                  <button
                    type="button"
                    aria-label={passwordVisible ? 'Hide password' : 'Show password'}
                    aria-pressed={passwordVisible}
                    disabled={disabled || readOnly}
                    onMouseDown={(e) => e.preventDefault()}
                    onClick={toggleVisibility}
                    className="text-muted-foreground hover:text-foreground focus-visible:ring-ring/50 shrink-0 rounded p-0.5 transition-colors focus-visible:ring-1 focus-visible:outline-none disabled:cursor-not-allowed"
                  >
                    {passwordVisible ? (
                      <Eye className="size-4" aria-hidden="true" />
                    ) : (
                      <EyeOff className="size-4" aria-hidden="true" />
                    )}
                  </button>
                </div>
              )}
            </div>
    
            {showStrength && currentValue && (
              <div className="flex flex-col gap-1.5">
                <div className="bg-muted h-1.5 w-full overflow-hidden rounded-full">
                  <div
                    className={cn('h-full rounded-full transition-all duration-300', strength.barColor)}
                    style={{ width: `${strength.percent}%` }}
                  />
                </div>
                <div className="flex items-center justify-between text-xs">
                  <span className={cn('font-medium capitalize', strength.color)}>{strength.label}</span>
                  {minLength > 0 && (
                    <span className={meetsMinLength ? 'text-[var(--success)]' : 'text-muted-foreground'}>
                      {currentValue.length} / {minLength} chars
                    </span>
                  )}
                </div>
              </div>
            )}
    
            {minLength > 0 && !showStrength && currentValue && (
              <p className="text-muted-foreground text-xs">Minimum {minLength} characters</p>
            )}
          </div>
        )
      },
    )
    PasswordInput.displayName = 'PasswordInput'
    
    export { PasswordInput }
  • components/ui/password-input/password-input.variants.ts 0.8 kB
    import type { VariantProps } from 'class-variance-authority'
    import { cva } from 'class-variance-authority'
    
    export const passwordInputVariants = cva(
      'flex w-full items-center gap-1.5 overflow-hidden border transition-[color,box-shadow] outline-none rounded-md',
      {
        variants: {
          size: {
            sm: 'h-8 text-xs',
            default: 'h-9 text-base md:text-sm',
            lg: 'h-11 text-base',
          },
          variant: {
            outlined: 'border-input bg-transparent shadow-xs',
            filled: 'border-transparent bg-muted/50 shadow-none',
            borderless: 'border-transparent bg-transparent shadow-none',
          },
        },
        defaultVariants: {
          size: 'default',
          variant: 'outlined',
        },
      },
    )
    
    export type PasswordInputVariants = VariantProps<typeof passwordInputVariants>
  • components/ui/password-input/index.ts 0.2 kB
    export { PasswordInput, type PasswordInputProps } from './PasswordInput'
    export { passwordInputVariants, type PasswordInputVariants } from './password-input.variants'

Raw manifest: https://uipkge.dev/r/react/password-input.json