Password Field
Password input component with visibility toggle and optional tooltip. Built on shadcn InputGroup for enhanced UI.
npx shadcn@latest add https://shuip.plvo.dev/r/rhf-password-field.json
pnpm dlx shadcn@latest add https://shuip.plvo.dev/r/rhf-password-field.json
bun x shadcn@latest add https://shuip.plvo.dev/r/rhf-password-field.json
'use client';import { Eye, EyeOff, InfoIcon } from 'lucide-react';import * as React from 'react';import type { FieldPath, FieldValues, UseFormRegisterReturn } from 'react-hook-form';import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form';import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput } from '@/components/ui/input-group';import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';export interface PasswordFieldProps<T extends FieldValues> extends React.ComponentProps<typeof InputGroupInput> {register: UseFormRegisterReturn<FieldPath<T>>;label?: string;description?: string;tooltip?: React.ReactNode;}export function PasswordField<T extends FieldValues>({register,label,description,tooltip,...props}: PasswordFieldProps<T>) {const [showPassword, setShowPassword] = React.useState(false);const handleTogglePassword = React.useCallback(() => {setShowPassword((prev) => !prev);}, []);return (<FormField{...register}render={({ field, fieldState }) => {return (<FormItem data-invalid={fieldState.invalid}>{label && <FormLabel>{label}</FormLabel>}<FormControl><InputGroup><InputGroupInput{...field}type={showPassword ? 'text' : 'password'}placeholder='Enter password'aria-invalid={fieldState.invalid}{...props}/><InputGroupAddon align='inline-end'><InputGroupButton aria-label='Toggle password' onClick={handleTogglePassword}>{showPassword ? <EyeOff className='size-4' /> : <Eye className='size-4' />}</InputGroupButton>{tooltip && (<Tooltip><TooltipTrigger asChild><InputGroupButton aria-label='Info' size='icon-xs'><InfoIcon /></InputGroupButton></TooltipTrigger><TooltipContent>{tooltip}</TooltipContent></Tooltip>)}</InputGroupAddon></InputGroup></FormControl><FormMessage className='text-xs text-left opacity-80' />{description && <FormDescription className='text-xs'>{description}</FormDescription>}</FormItem>);}}/>);}
Loading...
Password inputs require special UX considerations: users need to verify their input while maintaining security. PasswordField solves this with a built-in visibility toggle (Eye/EyeOff icons) that switches between masked and plain text.
The component uses shadcn's InputGroup to position the toggle button inline with the input, avoiding layout shifts. An optional tooltip can be added via the tooltip prop to display password requirements—this appears as an InfoIcon button next to the visibility toggle.
Built-in features
- One-click visibility toggle with Eye/EyeOff icons
- InputGroup positioning for seamless button integration
- Optional tooltip for password requirements or help text
- Zod validation: Native integration for password strength rules
Usage patterns
Common validation pattern for strong passwords:
const schema = z.object({
password: z.string()
.min(8, 'At least 8 characters required')
.regex(/[A-Z]/, 'Must include uppercase letter')
.regex(/[0-9]/, 'Must include number')
.regex(/[!@#$%^&*]/, 'Must include special character'),
})
const form = useForm({
defaultValues: { password: '' },
resolver: zodResolver(schema),
})
<PasswordField
register={form.register('password')}
label='Password'
tooltip='Must contain: 8+ characters, uppercase, number, special char'
/>Confirm password with linked validation:
const schema = z.object({
password: z.string().min(8),
confirmPassword: z.string(),
}).refine((data) => data.password === data.confirmPassword, {
message: "Passwords don't match",
path: ['confirmPassword'],
})
<PasswordField
register={form.register('password')}
label='Password'
/>
<PasswordField
register={form.register('confirmPassword')}
label='Confirm Password'
/>Examples
Confirm Password
Loading...
'use client';import { zodResolver } from '@hookform/resolvers/zod';import { useForm } from 'react-hook-form';import { z } from 'zod';import { Form } from '@/components/ui/form';import { PasswordField } from '@/components/ui/shuip/react-hook-form/password-field';import { SubmitButton } from '@/components/ui/shuip/submit-button';const zodSchema = z.object({password: z.string().min(8, 'Password must be at least 8 characters'),confirmPassword: z.string(),}).refine((data) => data.password === data.confirmPassword, {message: "Passwords don't match",path: ['confirmPassword'],});export default function RhfPasswordFieldConfirmPasswordExample() {const form = useForm({defaultValues: {password: '',confirmPassword: '',},resolver: zodResolver(zodSchema),});async function onSubmit(_values: z.infer<typeof zodSchema>) {try {alert(`Password set successfully!`);} catch (error) {console.error(error);}}return (<Form {...form}><form onSubmit={form.handleSubmit(onSubmit)} className='space-y-4'><PasswordFieldregister={form.register('password')}label='Password'description='Choose a strong password'placeholder='Enter password'/><PasswordFieldregister={form.register('confirmPassword')}label='Confirm Password'description='Re-enter your password to confirm'placeholder='Confirm password'/><SubmitButton>Set Password</SubmitButton></form></Form>);}
Default
Loading...
'use client';import { zodResolver } from '@hookform/resolvers/zod';import { useForm } from 'react-hook-form';import { z } from 'zod';import { Form } from '@/components/ui/form';import { PasswordField } from '@/components/ui/shuip/react-hook-form/password-field';import { SubmitButton } from '@/components/ui/shuip/submit-button';const zodSchema = z.object({password: z.string().nonempty({ message: 'Password is required' }).min(8, { message: 'Password must be at least 8 characters' }).regex(/[A-Z]/, { message: 'Password must contain at least one uppercase letter' }).regex(/[0-9]/, { message: 'Password must contain at least one number' }).regex(/[!@#$%^&*]/, { message: 'Password must contain at least one special character' }),});export default function InputFieldExample() {const form = useForm({defaultValues: { password: '' },resolver: zodResolver(zodSchema),});async function onSubmit(values: z.infer<typeof zodSchema>) {try {alert(`Hello ${values.password}`);} catch (error) {console.error(error);}}return (<Form {...form}><form onSubmit={form.handleSubmit(onSubmit)} className='space-y-4'><PasswordFieldregister={form.register('password')}label='Password'description='Your password'placeholder='Password'/><SubmitButton>Check</SubmitButton></form></Form>);}
Login
Loading...
'use client';import { zodResolver } from '@hookform/resolvers/zod';import { useForm } from 'react-hook-form';import { z } from 'zod';import { Card, CardContent, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';import { Form } from '@/components/ui/form';import { InputField } from '@/components/ui/shuip/react-hook-form/input-field';import { PasswordField } from '@/components/ui/shuip/react-hook-form/password-field';import { SubmitButton } from '@/components/ui/shuip/submit-button';const zodSchema = z.object({email: z.email({ message: 'Invalid email' }),password: z.string().nonempty({ message: 'Password is required' }),});export default function InputFieldExample() {const form = useForm({defaultValues: { email: '', password: '' },resolver: zodResolver(zodSchema),});async function onSubmit(values: z.infer<typeof zodSchema>) {try {alert(`Hello ${values.email} ${values.password}`);} catch (error) {console.error(error);}}return (<Form {...form}><form onSubmit={form.handleSubmit(onSubmit)} className='space-y-4'><Card><CardHeader><CardTitle>Login</CardTitle></CardHeader><CardContent className='space-y-4'><InputField register={form.register('email')} label='Email' placeholder='john@example.com' /><PasswordFieldregister={form.register('password')}label='Password'placeholder='Password'tooltip='Must contain: 8+ characters, uppercase, number, special char'/></CardContent><CardFooter><SubmitButton>Login</SubmitButton></CardFooter></Card></form></Form>);}
Strength Meter
Loading...
'use client';import { zodResolver } from '@hookform/resolvers/zod';import { useForm } from 'react-hook-form';import { z } from 'zod';import { Form } from '@/components/ui/form';import { PasswordField } from '@/components/ui/shuip/react-hook-form/password-field';import { SubmitButton } from '@/components/ui/shuip/submit-button';import { cn } from '@/lib/utils';// Calculate password strength score (0-4)function calculatePasswordStrength(password: string): number {let strength = 0;if (password.length >= 8) strength++;if (password.length >= 12) strength++;if (/[a-z]/.test(password) && /[A-Z]/.test(password)) strength++;if (/[0-9]/.test(password)) strength++;if (/[!@#$%^&*(),.?":{}|<>]/.test(password)) strength++;return Math.min(strength, 4);}function getStrengthLabel(strength: number): { label: string; color: string } {const labels = [{ label: 'Very Weak', color: 'bg-red-500' },{ label: 'Weak', color: 'bg-orange-500' },{ label: 'Fair', color: 'bg-yellow-500' },{ label: 'Good', color: 'bg-lime-500' },{ label: 'Strong', color: 'bg-green-500' },];return labels[strength] || labels[0];}const zodSchema = z.object({password: z.string().min(8, 'Password must be at least 8 characters').regex(/[A-Z]/, 'Must include uppercase letter').regex(/[a-z]/, 'Must include lowercase letter').regex(/[0-9]/, 'Must include number').regex(/[!@#$%^&*(),.?":{}|<>]/, 'Must include special character'),confirmPassword: z.string(),}).refine((data) => data.password === data.confirmPassword, {message: "Passwords don't match",path: ['confirmPassword'],});export default function RhfPasswordFieldStrengthMeterExample() {const form = useForm({defaultValues: {password: '',confirmPassword: '',},resolver: zodResolver(zodSchema),});const password = form.watch('password');const strength = password ? calculatePasswordStrength(password) : 0;const { label, color } = getStrengthLabel(strength);async function onSubmit(_values: z.infer<typeof zodSchema>) {try {alert(`Account created with password strength: ${label}`);} catch (error) {console.error(error);}}return (<Form {...form}><form onSubmit={form.handleSubmit(onSubmit)} className='space-y-4'><div className='space-y-2'><PasswordFieldregister={form.register('password')}label='Password'tooltip={<div className='space-y-1'><p className='font-semibold'>Password must contain:</p><ul className='list-disc list-inside text-sm space-y-0.5'><li>At least 8 characters (12+ recommended)</li><li>Uppercase and lowercase letters</li><li>At least one number</li><li>At least one special character</li></ul></div>}placeholder='Enter password'/>{/* Strength meter */}{password && (<div className='space-y-1.5'><div className='flex gap-1'>{[...Array(4)].map((_, i) => (<divkey={i}className={cn('h-1 flex-1 rounded-full transition-colors', i < strength ? color : 'bg-muted')}/>))}</div><p className='text-sm text-muted-foreground'>Strength: <span className='font-medium'>{label}</span></p></div>)}</div><PasswordFieldregister={form.register('confirmPassword')}label='Confirm Password'placeholder='Re-enter password'/><SubmitButton>Create Account</SubmitButton></form></Form>);}
Tooltip
Loading...
'use client';import { zodResolver } from '@hookform/resolvers/zod';import { useForm } from 'react-hook-form';import { z } from 'zod';import { Form } from '@/components/ui/form';import { PasswordField } from '@/components/ui/shuip/react-hook-form/password-field';import { SubmitButton } from '@/components/ui/shuip/submit-button';const zodSchema = z.object({password: z.string().nonempty({ message: 'Password is required' }).min(8, { message: 'Password must be at least 8 characters' }).regex(/[A-Z]/, { message: 'Password must contain at least one uppercase letter' }).regex(/[0-9]/, { message: 'Password must contain at least one number' }).regex(/[!@#$%^&*]/, { message: 'Password must contain at least one special character' }),});export default function InputFieldExample() {const form = useForm({defaultValues: { password: '' },resolver: zodResolver(zodSchema),});async function onSubmit(values: z.infer<typeof zodSchema>) {try {alert(`Hello ${values.password}`);} catch (error) {console.error(error);}}return (<Form {...form}><form onSubmit={form.handleSubmit(onSubmit)} className='space-y-4'><PasswordFieldregister={form.register('password')}label='Password'description='Your password'placeholder='Password'tooltip='Must contain: 8+ characters, uppercase, number, special char'/><SubmitButton>Check</SubmitButton></form></Form>);}
Props
Prop
Type