Forms & Inputs
Form controls for user input with TanStack Form integration.
Form components provide validated user input with TanStack Form and Zod integration. All form components are accessible and support dark mode.
Form
Forms use useForm from @tanstack/react-form together with the @kit/ui/field primitives. There is no <Form> provider wrapper — you render a plain <form> element and bind each input through form.Field render props. The Zod schema is passed to validators and drives both onChange and onSubmit validation.
import { useForm } from '@tanstack/react-form';import { Field, FieldError, FieldLabel, FieldDescription } from '@kit/ui/field';Basic Form
'use client';import { useForm } from '@tanstack/react-form';import * as z from 'zod';import { Field, FieldError, FieldLabel } from '@kit/ui/field';import { Input } from '@kit/ui/input';import { Button } from '@kit/ui/button';const schema = z.object({ email: z.string().email(), name: z.string().min(2),});function MyForm() { const form = useForm({ defaultValues: { email: '', name: '', }, validators: { onChange: schema, onSubmit: schema, }, onSubmit: ({ value }) => { // handle validated `value` }, }); return ( <form onSubmit={(e) => { e.preventDefault(); void form.handleSubmit(); }} > <form.Field name="email"> {(field) => { const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid; return ( <Field data-invalid={isInvalid}> <FieldLabel>Email</FieldLabel> <Input name={field.name} placeholder="you@example.com" value={field.state.value} onBlur={field.handleBlur} onChange={(e) => field.handleChange(e.target.value)} aria-invalid={isInvalid} /> <FieldError errors={field.state.meta.errors} /> </Field> ); }} </form.Field> <Button type="submit">Submit</Button> </form> );}FieldError from @kit/ui/field reads field.state.meta.errors and translates each message via i18n, so you don't render validation strings yourself.
Input
Basic text input field.
import { Input } from '@kit/ui/input';<Input placeholder="Enter text" /><Input type="email" placeholder="Email address" /><Input type="password" placeholder="Password" /><Input disabled placeholder="Disabled input" />Textarea
Multi-line text input.
import { Textarea } from '@kit/ui/textarea';<Textarea placeholder="Enter description..." /><Textarea rows={6} placeholder="Longer text area" />Select
Dropdown select with search and keyboard navigation.
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@kit/ui/select';<Select onValueChange={handleChange} defaultValue="option1"> <SelectTrigger> <SelectValue placeholder="Select option" /> </SelectTrigger> <SelectContent> <SelectItem value="option1">Option 1</SelectItem> <SelectItem value="option2">Option 2</SelectItem> <SelectItem value="option3">Option 3</SelectItem> </SelectContent></Select>With Form Field
<form.Field name="category"> {(field) => { const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid; return ( <Field data-invalid={isInvalid}> <FieldLabel>Category</FieldLabel> <Select value={field.state.value} onValueChange={(value) => field.handleChange(value)} > <SelectTrigger aria-invalid={isInvalid}> <SelectValue placeholder="Select category" /> </SelectTrigger> <SelectContent> <SelectItem value="work">Work</SelectItem> <SelectItem value="personal">Personal</SelectItem> </SelectContent> </Select> <FieldError errors={field.state.meta.errors} /> </Field> ); }}</form.Field>Checkbox
Boolean checkbox control.
import { Checkbox } from '@kit/ui/checkbox';<div className="flex items-center gap-2"> <Checkbox id="terms" /> <label htmlFor="terms">Accept terms and conditions</label></div>Radio Group
Single selection from multiple options.
import { RadioGroup, RadioGroupItem } from '@kit/ui/radio-group';<RadioGroup defaultValue="option1" onValueChange={handleChange}> <div className="flex items-center gap-2"> <RadioGroupItem value="option1" id="r1" /> <label htmlFor="r1">Option 1</label> </div> <div className="flex items-center gap-2"> <RadioGroupItem value="option2" id="r2" /> <label htmlFor="r2">Option 2</label> </div></RadioGroup>Switch
Toggle switch for boolean values.
import { Switch } from '@kit/ui/switch';<div className="flex items-center gap-2"> <Switch id="notifications" /> <label htmlFor="notifications">Enable notifications</label></div>{/* Small size */}<Switch size="sm" />Input OTP
One-time password input for verification codes.
import { InputOTP, InputOTPGroup, InputOTPSlot } from '@kit/ui/input-otp';<InputOTP maxLength={6}> <InputOTPGroup> <InputOTPSlot index={0} /> <InputOTPSlot index={1} /> <InputOTPSlot index={2} /> <InputOTPSlot index={3} /> <InputOTPSlot index={4} /> <InputOTPSlot index={5} /> </InputOTPGroup></InputOTP>Label
Accessible form label.
import { Label } from '@kit/ui/label';<Label htmlFor="email">Email address</Label><Input id="email" type="email" />Field
Form field wrapper with label and description.
import { Field, FieldLabel, FieldDescription, FieldError } from '@kit/ui/field';<Field> <FieldLabel>Username</FieldLabel> <Input placeholder="Enter username" /> <FieldDescription>Your unique username</FieldDescription> <FieldError>Username is required</FieldError></Field>Image Uploader
Drag-and-drop image upload with preview.
import { ImageUploader } from '@kit/ui/image-uploader';<ImageUploader value={imageUrl} onValueChange={(url) => setImageUrl(url)}> <span>Drop image here or click to upload</span></ImageUploader>Supports JPEG, PNG, GIF, WebP up to 5MB.
These form components are part of the TanStack Start Prisma SaaS Kit.
Next: Buttons & Actions →