Input Field
Text input component integrated with TanStack Form for type-safe form management. Supports tooltips and InputGroup integration for addons and buttons.
npx shadcn@latest add https://shuip.plvo.dev/r/tsf-input-field.json
pnpm dlx shadcn@latest add https://shuip.plvo.dev/r/tsf-input-field.json
bun x shadcn@latest add https://shuip.plvo.dev/r/tsf-input-field.json
import type {DeepKeys,DeepValue,FieldAsyncValidateOrFn,FieldOptions,FieldValidateOrFn,FormAsyncValidateOrFn,FormValidateOrFn,ReactFormApi,} from '@tanstack/react-form';import { InfoIcon } from 'lucide-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 InputFieldProps<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 InputField<TFormData, TName extends DeepKeys<TFormData>, TData extends DeepValue<TFormData, TName>>({form,name,label,description,formProps,fieldProps,props,tooltip,}: InputFieldProps<TFormData, TName, TData>) {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='text'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}/>{tooltip && (<InputGroupAddon align='inline-end'><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>);}
InputField is a text input component that encapsulates TanStack Form's field management with shadcn/ui's design system. It handles all the boilerplate of connecting form state to an input element: wiring event handlers, displaying errors, managing touched states, and rendering consistent UI.
This component is useful when you want to quickly add form inputs without manually setting up form.Field render props, input bindings, and error display logic for every field.
Built-in features
- Type-safe field names: Uses
DeepKeys<TFormData>for autocomplete and compile-time validation - Nested path support: Access deeply nested values like
user.profile.email - Tooltip integration: Optional InfoIcon button with tooltip content via
tooltipprop - InputGroup ready: Supports shadcn InputGroup for seamless addon integration
- Async validation: Built-in debouncing and loading states for API validation
- Full type inference: Field value types automatically inferred from form schema
Less boilerplate
TanStack Form's standard approach uses render props to access field state:
<form.Field
name="email"
validators={{
onChange: ({ value }) => !value.includes('@') ? 'Invalid email' : undefined
}}
>
{(field) => (
<>
<label htmlFor={field.name}>Email</label>
<input
id={field.name}
name={field.name}
type="email"
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
onBlur={field.handleBlur}
/>
{!field.state.meta.isValid && (
<span>{field.state.meta.errors.join(', ')}</span>
)}
</>
)}
</form.Field>With InputField, this reduces to a single declarative component:
<InputField
form={form}
name='email'
label='Email'
props={{ type: 'email' }}
formProps={{
validators: {
onChange: ({ value }) => !value.includes('@') ? 'Invalid email' : undefined
}
}}
/>Common use cases
const form = useForm({
defaultValues: {
name: '',
email: '',
age: 0,
},
onSubmit: async ({ value }) => {
await saveData(value)
},
})
// Basic text input
<InputField
form={form}
name='name'
label='Name'
description='Your full name'
/>
// Email with validation
<InputField
form={form}
name='email'
label='Email'
props={{ type: 'email' }}
formProps={{
validators: {
onChange: ({ value }) =>
!value.includes('@') ? 'Invalid email' : undefined
}
}}
/>
// With tooltip
<InputField
form={form}
name='username'
label='Username'
tooltip='Username must be unique and 3-20 characters'
formProps={{
validators: {
onChange: ({ value }) =>
value.length < 3 ? 'Too short' : undefined
}
}}
/>
// Number input
<InputField
form={form}
name='age'
label='Age'
props={{ type: 'number', min: 0, max: 120 }}
/>Examples
Async Validation
'use client';import { useForm } from '@tanstack/react-form';import { InputField } from '@/components/ui/shuip/tanstack-form/input-field';import { SubmitButton } from '@/components/ui/shuip/tanstack-form/submit-button';// Simulate API call to check username availabilityasync function checkUsernameAvailability(username: string): Promise<boolean> {await new Promise((resolve) => setTimeout(resolve, 1000));const takenUsernames = ['admin', 'user', 'test', 'demo'];return !takenUsernames.includes(username.toLowerCase());}export default function TsfInputFieldAsyncValidationExample() {const form = useForm({defaultValues: {username: '',email: '',},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'><InputFieldform={form}name='username'label='Username'description='Username will be checked for availability'props={{ placeholder: 'Enter username' }}formProps={{validators: {// Sync validation: runs immediatelyonChange: ({ value }) => {if (!value) return 'Username is required';if (value.length < 3) return 'Username must be at least 3 characters';if (!/^[a-zA-Z0-9_]+$/.test(value)) return 'Only letters, numbers, and underscores allowed';return undefined;},// Async validation: checks availability via APIonChangeAsync: async ({ value }) => {if (!value || value.length < 3) return undefined;const available = await checkUsernameAvailability(value);return available ? undefined : 'Username is already taken';},// Debounce async validation to avoid excessive API callsonChangeAsyncDebounceMs: 500,},}}/><InputFieldform={form}name='email'label='Email'description='Email will be validated on blur'props={{ type: 'email', placeholder: 'your@email.com' }}formProps={{validators: {onBlur: ({ value }) => {if (!value) return 'Email is required';if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) return 'Invalid email format';return undefined;},},}}/><SubmitButton form={form}>Create Account</SubmitButton></form>);}
Default
'use client';import { useForm } from '@tanstack/react-form';import { InputField } from '@/components/ui/shuip/tanstack-form/input-field';import { SubmitButton } from '@/components/ui/shuip/tanstack-form/submit-button';export default function TsfInputFieldExample() {const form = useForm({defaultValues: {name: '',email: '',},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'><InputFieldform={form}name='name'label='Name'description='Your full name'formProps={{validators: {onChange: ({ value }) => (value.length < 3 ? 'Name must be at least 3 characters' : undefined),},}}/><InputFieldform={form}name='email'label='Email'props={{ type: 'email' }}formProps={{validators: {onChange: ({ value }) => (!value.includes('@') ? 'Invalid email address' : undefined),},}}/><SubmitButton form={form} props={{ variant: 'outline' }}>Register</SubmitButton></form>);}
Nested Path
'use client';import { useForm } from '@tanstack/react-form';import { InputField } from '@/components/ui/shuip/tanstack-form/input-field';import { SubmitButton } from '@/components/ui/shuip/tanstack-form/submit-button';export default function TsfInputFieldNestedPathExample() {const form = useForm({defaultValues: {user: {email: '',profile: {firstName: '',lastName: '',bio: '',},address: {street: '',city: '',zipCode: '',},},},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-6'><div className='space-y-4'><h3 className='text-lg font-semibold'>Account</h3><InputFieldform={form}name='user.email'label='Email Address'props={{ type: 'email' }}formProps={{validators: {onChange: ({ value }) => {if (!value) return 'Email is required';if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) return 'Invalid email format';return undefined;},},}}/></div><div className='space-y-4'><h3 className='text-lg font-semibold'>Profile</h3><div className='grid grid-cols-2 gap-4'><InputFieldform={form}name='user.profile.firstName'label='First Name'formProps={{validators: {onChange: ({ value }) => (!value ? 'First name is required' : undefined),},}}/><InputFieldform={form}name='user.profile.lastName'label='Last Name'formProps={{validators: {onChange: ({ value }) => (!value ? 'Last name is required' : undefined),},}}/></div><InputFieldform={form}name='user.profile.bio'label='Bio'description='Tell us about yourself'props={{ placeholder: 'Software developer from...' }}/></div><div className='space-y-4'><h3 className='text-lg font-semibold'>Address</h3><InputFieldform={form}name='user.address.street'label='Street Address'formProps={{validators: {onChange: ({ value }) => (!value ? 'Street address is required' : undefined),},}}/><div className='grid grid-cols-2 gap-4'><InputField form={form} name='user.address.city' label='City' /><InputFieldform={form}name='user.address.zipCode'label='ZIP Code'props={{ placeholder: '12345' }}formProps={{validators: {onChange: ({ value }) => {if (!value) return undefined;if (!/^\d{5}(-\d{4})?$/.test(value)) return 'Invalid ZIP code format';return undefined;},},}}/></div></div><SubmitButton form={form}>Save Profile</SubmitButton></form>);}
Tooltip
'use client';import { useForm } from '@tanstack/react-form';import { InputField } from '@/components/ui/shuip/tanstack-form/input-field';import { SubmitButton } from '@/components/ui/shuip/tanstack-form/submit-button';export default function TsfInputFieldTooltipExample() {const form = useForm({defaultValues: {apiKey: '',webhookUrl: '',secretKey: '',},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'><InputFieldform={form}name='apiKey'label='API Key'description='Your application API key'tooltip={<><p className='font-semibold mb-1'>Where to find your API key:</p><ol className='list-decimal list-inside space-y-1 text-sm'><li>Go to Settings → API</li><li>Click "Generate New Key"</li><li>Copy the key (it will only be shown once)</li></ol></>}props={{ placeholder: 'sk_live_...' }}formProps={{validators: {onChange: ({ value }) => {if (!value) return 'API key is required';if (!value.startsWith('sk_')) return 'API key must start with sk_';if (value.length < 20) return 'API key is too short';return undefined;},},}}/><InputFieldform={form}name='webhookUrl'label='Webhook URL'description='Endpoint to receive webhook events'tooltip='This URL must be publicly accessible and accept POST requests. We recommend using HTTPS for security.'props={{ type: 'url', placeholder: 'https://api.example.com/webhooks' }}formProps={{validators: {onChange: ({ value }) => {if (!value) return 'Webhook URL is required';if (!value.startsWith('https://')) return 'Webhook URL must use HTTPS';try {new URL(value);return undefined;} catch {return 'Invalid URL format';}},},}}/><InputFieldform={form}name='secretKey'label='Webhook Secret'description='Used to verify webhook signatures'tooltip='Keep this secret safe. It will be used to sign webhook payloads so you can verify their authenticity.'props={{ placeholder: 'whsec_...' }}formProps={{validators: {onChange: ({ value }) => {if (!value) return 'Webhook secret is required';if (value.length < 16) return 'Secret must be at least 16 characters';return undefined;},},}}/><SubmitButton form={form}>Save Configuration</SubmitButton></form>);}
Props
Prop
Type