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'>
<PasswordField
register={form.register('password')}
label='Password'
description='Choose a strong password'
placeholder='Enter password'
/>
<PasswordField
register={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'>
<PasswordField
register={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' />
<PasswordField
register={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'>
<PasswordField
register={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) => (
<div
key={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>
<PasswordField
register={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'>
<PasswordField
register={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

On this page