Password Input
An input that can toggle visibility of the password text and check the strength of the password.
import { PasswordInput } from "@/components/ui/password-input"
export function BasicPasswordInput() { return <PasswordInput defaultValue="Password" className="w-[250px]" />}
Installation
Section titled “Installation”Install the following dependencies.
Copy and paste the following code into your project.
components/ui/password-input.tsx
"use client"
import { Button } from "@/components/ui/button"import { Input } from "@/components/ui/input"import { cn } from "@/lib/utils"import { EyeIcon, EyeOffIcon } from "lucide-react"import { useState, createContext, useContext, type ComponentProps, type ReactNode, type ChangeEvent, useEffect, useDeferredValue, useMemo,} from "react"import { zxcvbn, zxcvbnOptions } from "@zxcvbn-ts/core"import { Tooltip, TooltipContent, TooltipTrigger,} from "@/components/ui/tooltip"
const PasswordInputContext = createContext<{ password: string } | null>(null)
export function PasswordInput({ className, children, onChange, value, defaultValue, ...props}: Omit<ComponentProps<typeof Input>, "type"> & { children?: ReactNode}) { const [showPassword, setShowPassword] = useState(false) const [password, setPassword] = useState(defaultValue ?? "")
const Icon = showPassword ? EyeOffIcon : EyeIcon const currentValue = value ?? password
const handleChange = (e: ChangeEvent<HTMLInputElement>) => { setPassword(e.target.value) onChange?.(e) }
return ( <PasswordInputContext value={{ password: currentValue.toString() }}> <div className="space-y-3"> <div className="relative"> <Input {...props} value={value} defaultValue={defaultValue} type={showPassword ? "text" : "password"} className={cn("pr-9", className)} onChange={handleChange} /> <Button variant="ghost" size="icon" type="button" className="absolute inset-y-1/2 right-1 size-7 -translate-y-1/2" onClick={() => setShowPassword(p => !p)} > <Icon className="size-5" /> <span className="sr-only"> {showPassword ? "Hide password" : "Show password"} </span> </Button> </div> {children} </div> </PasswordInputContext> )}
export function PasswordInputStrengthChecker() { const [optionsLoaded, setOptionsLoaded] = useState(false) const [errorLoadingOptions, setErrorLoadingOptions] = useState(false)
const { password } = usePasswordInput() const deferredPassword = useDeferredValue(password) const strengthResult = useMemo(() => { if (!optionsLoaded || deferredPassword.length === 0) { return { score: 0, feedback: { warning: undefined } } as const }
return zxcvbn(deferredPassword) }, [optionsLoaded, deferredPassword])
useEffect(() => { Promise.all([ import("@zxcvbn-ts/language-common"), import("@zxcvbn-ts/language-en"), ]) .then(([common, english]) => { zxcvbnOptions.setOptions({ translations: english.translations, graphs: common.adjacencyGraphs, maxLength: 50, dictionary: { ...common.dictionary, ...english.dictionary, }, }) setOptionsLoaded(true) }) .catch(() => setErrorLoadingOptions(true)) }, [])
function getLabel() { if (deferredPassword.length === 0) return "Password strength" if (!optionsLoaded) return "Loading strength checker"
const score = strengthResult.score switch (score) { case 0: case 1: return "Very weak" case 2: return "Weak" case 3: return "Strong" case 4: return "Very strong" default: throw new Error(`Invalid score: ${score satisfies never}`) } }
const label = getLabel()
if (errorLoadingOptions) return null
return ( <div className="space-y-0.5"> <div role="progressbar" aria-label="Password Strength" aria-valuenow={strengthResult.score} aria-valuemin={0} aria-valuemax={4} aria-valuetext={label} className="flex gap-1" > {Array.from({ length: 4 }).map((_, i) => { const color = strengthResult.score >= 3 ? "bg-primary" : "bg-destructive"
return ( <div key={i} className={cn( "h-1 flex-1 rounded-full", strengthResult.score > i ? color : "bg-secondary", )} /> ) })} </div> <div className="flex justify-end text-sm text-muted-foreground"> {strengthResult.feedback.warning == null ? ( label ) : ( <Tooltip> <TooltipTrigger className="underline underline-offset-1"> {label} </TooltipTrigger> <TooltipContent side="bottom" sideOffset={4} className="text-base"> {strengthResult.feedback.warning} </TooltipContent> </Tooltip> )} </div> </div> )}
const usePasswordInput = () => { const context = useContext(PasswordInputContext) if (context == null) { throw new Error( "usePasswordInput must be used within a PasswordInputContext", ) } return context}
Update the import paths to match your project setup.
import { useState } from "react"import { PasswordInput } from "@/components/ui/PasswordInput"
<PasswordInput />
Examples
Section titled “Examples”"use client"
import { zodResolver } from "@hookform/resolvers/zod"import { useForm } from "react-hook-form"import { toast } from "sonner"import { z } from "zod"import { Button } from "@/components/ui/button"import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage,} from "@/components/ui/form"import { PasswordInput } from "@/components/ui/password-input"
const formSchema = z.object({ password: z.string().min(8, "Password must be at least 8 characters long"),})
export function PasswordInputForm() { const form = useForm<z.infer<typeof formSchema>>({ resolver: zodResolver(formSchema), defaultValues: { password: "", }, })
function onSubmit(data: z.infer<typeof formSchema>) { toast("You submitted the following values", { description: ( <pre className="mt-2 w-[320px] rounded-md bg-neutral-950 p-4"> <code className="text-white">{JSON.stringify(data, null, 2)}</code> </pre> ), }) }
return ( <Form {...form}> <form onSubmit={form.handleSubmit(onSubmit)} className="w-2/3 space-y-6"> <FormField control={form.control} name="password" render={({ field }) => ( <FormItem> <FormLabel>Password</FormLabel> <FormControl> <PasswordInput {...field} /> </FormControl> <FormMessage /> </FormItem> )} /> <Button type="submit">Submit</Button> </form> </Form> )}
Password Input Strength Checker
Section titled “Password Input Strength Checker”import { PasswordInput, PasswordInputStrengthChecker,} from "../../items/password-input/components/password-input"
export default function PasswordInputStrength() { return ( <PasswordInput> <PasswordInputStrengthChecker /> </PasswordInput> )}