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 { 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 { useFieldContext } from '@/components/ui/shuip/tanstack-form/form-context';import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';export interface PasswordFieldProps {label?: string;description?: string;fieldProps?: React.ComponentProps<typeof Field>;props?: React.ComponentProps<'input'>;tooltip?: React.ReactNode;}export function PasswordField({ label, description, fieldProps, props, tooltip }: PasswordFieldProps) {const field = useFieldContext<string>();const { isValid, errors } = field.state.meta;const [showPassword, setShowPassword] = React.useState(false);const handleTogglePassword = React.useCallback(() => {setShowPassword((prev) => !prev);}, []);return (<Field className='gap-2' data-invalid={!isValid} {...fieldProps}>{label && <FieldLabel htmlFor={field.name}>{label}</FieldLabel>}<InputGroup><InputGroupInputid={field.name}type={showPassword ? 'text' : 'password'}placeholder='Enter password'name={field.name}value={field.state.value}onChange={(e) => field.handleChange(e.target.value)}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 && (<FieldErrorclassName='text-xs text-left'errors={errors.map((error) => ({ message: typeof error === 'string' ? error : error?.message }))}/>)}{description && <FieldDescription className='text-xs'>{description}</FieldDescription>}</Field>);}
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 reads the surrounding field via useFieldContext, so you compose it inside a <form.AppField>. It 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
- Context-bound field state via
useFieldContext— no prop drilling - 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
Setup
Field components are bound via React context. In your project, create lib/form.ts once:
// lib/form.ts
import { createFormHook } from '@tanstack/react-form';
import { fieldContext, formContext } from '@/components/ui/shuip/tanstack-form/form-context';
import { PasswordField } from '@/components/ui/shuip/tanstack-form/password-field';
import { SubmitButton } from '@/components/ui/shuip/tanstack-form/submit-button';
export const { useAppForm } = createFormHook({
fieldContext,
formContext,
fieldComponents: { PasswordField },
formComponents: { SubmitButton },
});See the form-context item for details.
Password security patterns
Common validation pattern for strong passwords:
import { useAppForm } from '@/lib/form';
const form = useAppForm({
defaultValues: { password: '', confirmPassword: '' },
onSubmit: async ({ value }) => {
await saveData(value);
},
});
<form.AppField
name='password'
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;
},
}}
children={(field) => (
<field.PasswordField
label='Password'
tooltip='Must contain: 8+ characters, uppercase, number, special char'
/>
)}
/>Confirm password with linked validation:
<form.AppField
name='confirmPassword'
validators={{
onChangeListenTo: ['password'],
onChange: ({ value, fieldApi }) => {
const password = fieldApi.form.getFieldValue('password');
return value !== password ? 'Passwords do not match' : undefined;
},
}}
children={(field) => (
<field.PasswordField label='Confirm Password' />
)}
/>Examples
Confirm Password
'use client';import { createFormHook } from '@tanstack/react-form';import { fieldContext, formContext } from '@/components/ui/shuip/tanstack-form/form-context';import { PasswordField } from '@/components/ui/shuip/tanstack-form/password-field';import { SubmitButton } from '@/components/ui/shuip/tanstack-form/submit-button';const { useAppForm } = createFormHook({fieldContext,formContext,fieldComponents: { PasswordField },formComponents: { SubmitButton },});export default function TsfPasswordFieldExample() {const form = useAppForm({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'><form.AppFieldname='password'validators={{onChange: ({ value }) => (value.length < 8 ? 'Password must be at least 8 characters' : undefined),}}children={(field) => <field.PasswordField label='Password' />}/><form.AppFieldname='confirmPassword'validators={{onChangeListenTo: ['password'],onChange: ({ value, fieldApi }) => {const password = fieldApi.form.getFieldValue('password');if (value !== password) return 'Passwords do not match';return undefined;},}}children={(field) => <field.PasswordField label='Confirm Password' />}/><form.AppForm><form.SubmitButton /></form.AppForm></form>);}
Default
'use client';import { createFormHook } from '@tanstack/react-form';import { fieldContext, formContext } from '@/components/ui/shuip/tanstack-form/form-context';import { PasswordField } from '@/components/ui/shuip/tanstack-form/password-field';import { SubmitButton } from '@/components/ui/shuip/tanstack-form/submit-button';const { useAppForm } = createFormHook({fieldContext,formContext,fieldComponents: { PasswordField },formComponents: { SubmitButton },});export default function TsfPasswordFieldExample() {const form = useAppForm({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'><form.AppFieldname='password'validators={{onChange: ({ value }) => (value.length < 8 ? 'Password must be at least 8 characters' : undefined),}}children={(field) => <field.PasswordField label='Password' />}/><form.AppForm><form.SubmitButton /></form.AppForm></form>);}
Strength Meter
'use client';import { createFormHook, useStore } from '@tanstack/react-form';import { fieldContext, formContext } from '@/components/ui/shuip/tanstack-form/form-context';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];}const { useAppForm } = createFormHook({fieldContext,formContext,fieldComponents: { PasswordField },formComponents: { SubmitButton },});export default function TsfPasswordFieldStrengthMeterExample() {const form = useAppForm({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'><form.AppFieldname='password'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;},}}children={(field) => (<field.PasswordFieldlabel='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' }}/>)}/>{/* 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><form.AppFieldname='confirmPassword'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;},}}children={(field) => (<field.PasswordField label='Confirm Password' props={{ placeholder: 'Re-enter password' }} />)}/><form.AppForm><form.SubmitButton>Create Account</form.SubmitButton></form.AppForm></form>);}
Tooltip
'use client';import { createFormHook } from '@tanstack/react-form';import { fieldContext, formContext } from '@/components/ui/shuip/tanstack-form/form-context';import { PasswordField } from '@/components/ui/shuip/tanstack-form/password-field';import { SubmitButton } from '@/components/ui/shuip/tanstack-form/submit-button';const { useAppForm } = createFormHook({fieldContext,formContext,fieldComponents: { PasswordField },formComponents: { SubmitButton },});export default function TsfPasswordFieldExample() {const form = useAppForm({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'><form.AppFieldname='password'validators={{onChange: ({ value }) => (value.length < 8 ? 'Password must be at least 8 characters' : undefined),}}children={(field) => (<field.PasswordField label='Password' tooltip='Use a strong password with letters, numbers, and symbols' />)}/><form.AppForm><form.SubmitButton /></form.AppForm></form>);}
Props
Prop
Type