# Forms & Inputs

> Form controls for user input with TanStack Form integration.

*Canonical: https://makerkit.dev/docs/tanstack-drizzle/ui-components/forms-and-inputs*

---

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.

```typescript
import { useForm } from '@tanstack/react-form';
import { Field, FieldError, FieldLabel, FieldDescription } from '@kit/ui/field';
```

### Basic Form

```tsx
'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.

```typescript
import { Input } from '@kit/ui/input';
```

```tsx
<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.

```typescript
import { Textarea } from '@kit/ui/textarea';
```

```tsx
<Textarea placeholder="Enter description..." />
<Textarea rows={6} placeholder="Longer text area" />
```

## Select

Dropdown select with search and keyboard navigation.

```typescript
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@kit/ui/select';
```

```tsx
<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

```tsx
<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.

```typescript
import { Checkbox } from '@kit/ui/checkbox';
```

```tsx
<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.

```typescript
import { RadioGroup, RadioGroupItem } from '@kit/ui/radio-group';
```

```tsx
<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.

```typescript
import { Switch } from '@kit/ui/switch';
```

```tsx
<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.

```typescript
import { InputOTP, InputOTPGroup, InputOTPSlot } from '@kit/ui/input-otp';
```

```tsx
<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.

```typescript
import { Label } from '@kit/ui/label';
```

```tsx
<Label htmlFor="email">Email address</Label>
<Input id="email" type="email" />
```

## Field

Form field wrapper with label and description.

```typescript
import { Field, FieldLabel, FieldDescription, FieldError } from '@kit/ui/field';
```

```tsx
<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.

```typescript
import { ImageUploader } from '@kit/ui/image-uploader';
```

```tsx
<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 Drizzle SaaS Kit](/drizzle).

---

**Next:** [Buttons & Actions →](./buttons-and-actions)
