Password Input
password-input ui 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
$ pnpm dlx shadcn@latest add https://uipkge.dev/r/react/password-input.json $ npx shadcn@latest add https://uipkge.dev/r/react/password-input.json $ yarn dlx shadcn@latest add https://uipkge.dev/r/react/password-input.json $ bunx 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
} npm dependencies
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