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”This installation provides support for all official Shadcn icon libraries. The component is otherwise identical to the non-experimental installation.
Icon support is experimental and may not be fully stable since it uses internal Shadcn APIs.
This component relies on other items which must be installed first.
Install the following dependencies.
Copy and paste the following code into your project.
components/ui/password-input.tsx
"use client"
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"import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput,} from "@/components/ui/input-group"
const PasswordInputContext = createContext<{ password: string } | null>(null)
export function PasswordInput({ 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"> <InputGroup> <InputGroupInput {...props} value={value} defaultValue={defaultValue} type={showPassword ? "text" : "password"} onChange={handleChange} /> <InputGroupAddon align="inline-end"> <InputGroupButton size="icon-xs" onClick={() => setShowPassword(p => !p)} > <Icon className="size-4.5" /> <span className="sr-only"> {showPassword ? "Hide password" : "Show password"} </span> </InputGroupButton> </InputGroupAddon> </InputGroup> {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”Password strength
import { PasswordInput, PasswordInputStrengthChecker,} from "@/components/ui/password-input"
export default function PasswordInputStrength() { return ( <PasswordInput> <PasswordInputStrengthChecker /> </PasswordInput> )}