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/tsf-password-field.json
pnpm dlx shadcn@latest add https://shuip.plvo.dev/r/tsf-password-field.json
bun x shadcn@latest add https://shuip.plvo.dev/r/tsf-password-field.json
'use client';import type {DeepKeys,DeepValue,FieldAsyncValidateOrFn,FieldOptions,FieldValidateOrFn,FormAsyncValidateOrFn,FormValidateOrFn,ReactFormApi,} from '@tanstack/react-form';import { Eye, EyeOff, InfoIcon } from 'lucide-react';import React from 'react';import { Field, FieldDescription, FieldError, FieldLabel } from '@/components/ui/field';import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput } from '@/components/ui/input-group';import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';export interface PasswordFieldProps<TFormData,TName extends DeepKeys<TFormData>,TData extends DeepValue<TFormData, TName>,> {form: ReactFormApi<TFormData,undefined | FormValidateOrFn<TFormData>,undefined | FormValidateOrFn<TFormData>,undefined | FormAsyncValidateOrFn<TFormData>,undefined | FormValidateOrFn<TFormData>,undefined | FormAsyncValidateOrFn<TFormData>,undefined | FormValidateOrFn<TFormData>,undefined | FormAsyncValidateOrFn<TFormData>,undefined | FormValidateOrFn<TFormData>,undefined | FormAsyncValidateOrFn<TFormData>,undefined | FormAsyncValidateOrFn<TFormData>,any>;name: TName;label?: string;description?: string;formProps?: Partial<FieldOptions<TFormData,TName,TData,undefined | FieldValidateOrFn<TFormData, TName, TData>,undefined | FieldValidateOrFn<TFormData, TName, TData>,undefined | FieldAsyncValidateOrFn<TFormData, TName, TData>,undefined | FieldValidateOrFn<TFormData, TName, TData>,undefined | FieldAsyncValidateOrFn<TFormData, TName, TData>,undefined | FieldValidateOrFn<TFormData, TName, TData>,undefined | FieldAsyncValidateOrFn<TFormData, TName, TData>,undefined | FieldValidateOrFn<TFormData, TName, TData>,undefined | FieldAsyncValidateOrFn<TFormData, TName, TData>>>;fieldProps?: React.ComponentProps<typeof Field>;props?: React.ComponentProps<'input'>;tooltip?: React.ReactNode;}export function PasswordField<TFormData, TName extends DeepKeys<TFormData>, TData extends DeepValue<TFormData, TName>>({form,name,label,description,formProps,fieldProps,props,tooltip,}: PasswordFieldProps<TFormData, TName, TData>) {const [showPassword, setShowPassword] = React.useState(false);const handleTogglePassword = React.useCallback(() => {setShowPassword((prev) => !prev);}, []);return (<form.Field name={name} {...formProps}>{(field) => {const { isValid, errors } = field.state.meta;return (<Field className='gap-2' data-invalid={!isValid} {...fieldProps}>{label && <FieldLabel>{label}</FieldLabel>}<InputGroup><InputGroupInputtype={showPassword ? 'text' : 'password'}placeholder='Enter password'name={field.name}value={field.state.value as string}onChange={(e) => field.handleChange(e.target.value as TData)}onBlur={field.handleBlur}aria-invalid={!isValid}{...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>{!isValid && (<FieldError className='text-xs text-left' errors={errors.map((error) => ({ message: error }))} />)}{description && <FieldDescription className='text-xs'>{description}</FieldDescription>}</Field>);}}</form.Field>);}
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
- Linked field validation via
onChangeListenTofor confirmation fields
Password security patterns
Common validation pattern for strong passwords:
<PasswordField
form={form}
name='password'
label='Password'
tooltip='Must contain: 8+ characters, uppercase, number, special char'
formProps={{
validators: {
onChange: ({ value }) => {
if (value.length < 8) return 'At least 8 characters required'
if (!/[A-Z]/.test(value)) return 'Must include uppercase letter'
if (!/[0-9]/.test(value)) return 'Must include number'
if (!/[!@#$%^&*]/.test(value)) return 'Must include special character'
return undefined
}
}
}}
/>Confirm password with linked validation:
<PasswordField
form={form}
name='confirmPassword'
label='Confirm Password'
formProps={{
validators: {
onChangeListenTo: ['password'],
onChange: ({ value, fieldApi }) => {
const password = fieldApi.form.getFieldValue('password')
return value !== password ? 'Passwords do not match' : undefined
}
}
}}
/>Examples
Confirm Password
Loading...
'use client';import { useForm } from '@tanstack/react-form';import { PasswordField } from '@/components/ui/shuip/tanstack-form/password-field';import { SubmitButton } from '@/components/ui/shuip/tanstack-form/submit-button';export default function TsfPasswordFieldExample() {const form = useForm({defaultValues: {password: '',confirmPassword: '',},onSubmit: async ({ value }) => {await new Promise((resolve) => setTimeout(resolve, 1000));alert(JSON.stringify(value, null, 2));},});return (<formonSubmit={(e) => {e.preventDefault();form.handleSubmit();}}className='space-y-4'><PasswordFieldform={form}name='password'label='Password'formProps={{validators: {onChange: ({ value }) => (value.length < 8 ? 'Password must be at least 8 characters' : undefined),},}}/><PasswordFieldform={form}name='confirmPassword'label='Confirm Password'formProps={{validators: {onChangeListenTo: ['password'],onChange: ({ value, fieldApi }) => {const password = fieldApi.form.getFieldValue('password');if (value !== password) return 'Passwords do not match';return undefined;},},}}/><SubmitButton form={form} /></form>);}
Default
Loading...
'use client';import { useForm } from '@tanstack/react-form';import { PasswordField } from '@/components/ui/shuip/tanstack-form/password-field';import { SubmitButton } from '@/components/ui/shuip/tanstack-form/submit-button';export default function TsfPasswordFieldExample() {const form = useForm({defaultValues: {password: '',},onSubmit: async ({ value }) => {await new Promise((resolve) => setTimeout(resolve, 1000));alert(JSON.stringify(value, null, 2));},});return (<formonSubmit={(e) => {e.preventDefault();form.handleSubmit();}}className='space-y-4'><PasswordFieldform={form}name='password'label='Password'formProps={{validators: {onChange: ({ value }) => (value.length < 8 ? 'Password must be at least 8 characters' : undefined),},}}/><SubmitButton form={form} /></form>);}
Strength Meter
Loading...
'use client';import { useForm, useStore } from '@tanstack/react-form';import { PasswordField } from '@/components/ui/shuip/tanstack-form/password-field';import { SubmitButton } from '@/components/ui/shuip/tanstack-form/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];}export default function TsfPasswordFieldStrengthMeterExample() {const form = useForm({defaultValues: {password: '',confirmPassword: '',},onSubmit: async ({ value }) => {await new Promise((resolve) => setTimeout(resolve, 1000));alert(JSON.stringify(value, null, 2));},});// Subscribe to password value changesconst password = useStore(form.store, (state) => state.values.password);const strength = password ? calculatePasswordStrength(password) : 0;const { label, color } = getStrengthLabel(strength);return (<formonSubmit={(e) => {e.preventDefault();form.handleSubmit();}}className='space-y-4'><div className='space-y-2'><PasswordFieldform={form}name='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>}props={{ placeholder: 'Enter password' }}formProps={{validators: {onChange: ({ value }) => {if (!value) return 'Password is required';if (value.length < 8) return 'Password must be at least 8 characters';if (!/[A-Z]/.test(value)) return 'Must include uppercase letter';if (!/[a-z]/.test(value)) return 'Must include lowercase letter';if (!/[0-9]/.test(value)) return 'Must include number';if (!/[!@#$%^&*(),.?":{}|<>]/.test(value)) return 'Must include special character';return undefined;},},}}/>{/* 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><PasswordFieldform={form}name='confirmPassword'label='Confirm Password'props={{ placeholder: 'Re-enter password' }}formProps={{validators: {onChangeListenTo: ['password'],onChange: ({ value, fieldApi }) => {const pwd = fieldApi.form.getFieldValue('password');if (!value) return 'Please confirm your password';if (value !== pwd) return 'Passwords do not match';return undefined;},},}}/><SubmitButton form={form}>Create Account</SubmitButton></form>);}
Tooltip
Loading...
'use client';import { useForm } from '@tanstack/react-form';import { PasswordField } from '@/components/ui/shuip/tanstack-form/password-field';import { SubmitButton } from '@/components/ui/shuip/tanstack-form/submit-button';export default function TsfPasswordFieldExample() {const form = useForm({defaultValues: {password: '',},onSubmit: async ({ value }) => {await new Promise((resolve) => setTimeout(resolve, 1000));alert(JSON.stringify(value, null, 2));},});return (<formonSubmit={(e) => {e.preventDefault();form.handleSubmit();}}className='space-y-4'><PasswordFieldform={form}name='password'label='Password'formProps={{validators: {onChange: ({ value }) => (value.length < 8 ? 'Password must be at least 8 characters' : undefined),},}}tooltip='Use a strong password with letters, numbers, and symbols'/><SubmitButton form={form} /></form>);}
Props
Prop
Type