Skip to main content

Overview

The Form component system provides a set of composable components for building forms with validation, error messages, and accessible markup. Built on top of react-hook-form and Radix UI.

Import

import { 
  Form, 
  FormField, 
  FormItem, 
  FormLabel, 
  FormControl, 
  FormDescription, 
  FormMessage 
} from '@repo/ui/form'

Usage

Basic Form

import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'
import { 
  Form, 
  FormField, 
  FormItem, 
  FormLabel, 
  FormControl, 
  FormMessage 
} from '@repo/ui/form'
import { Input } from '@repo/ui/input'
import { Button } from '@repo/ui/button'

const formSchema = z.object({
  email: z.string().email(),
})

export default function Example() {
  const form = useForm<z.infer<typeof formSchema>>({
    resolver: zodResolver(formSchema),
    defaultValues: {
      email: '',
    },
  })

  const onSubmit = (data: z.infer<typeof formSchema>) => {
    console.log(data)
  }

  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(onSubmit)}>
        <FormField
          control={form.control}
          name="email"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Email</FormLabel>
              <FormControl>
                <Input {...field} type="email" placeholder="Email" />
              </FormControl>
              <FormMessage />
            </FormItem>
          )}
        />
        <Button type="submit">Submit</Button>
      </form>
    </Form>
  )
}

With Description

import { Form, FormField, FormItem, FormLabel, FormControl, FormDescription, FormMessage } from '@repo/ui/form'
import { Input } from '@repo/ui/input'

export default function Example({ form }) {
  return (
    <FormField
      control={form.control}
      name="username"
      render={({ field }) => (
        <FormItem>
          <FormLabel>Username</FormLabel>
          <FormControl>
            <Input {...field} placeholder="johndoe" />
          </FormControl>
          <FormDescription>
            This is your public display name.
          </FormDescription>
          <FormMessage />
        </FormItem>
      )}
    />
  )
}

Components

Form

The root form component that provides form context.
<Form {...form}>
  {/* form fields */}
</Form>
This is an alias for FormProvider from react-hook-form.

FormField

Wraps form fields and provides field-level context.
control
Control
required
The form control object from useForm.
name
string
required
The name of the field in the form data.
render
(props) => ReactNode
required
Render function that receives field props and returns the field UI.

FormItem

Container for a form field, label, description, and error message.
className
string
Additional CSS classes to apply to the form item container.

FormLabel

Label element for the form field with error state styling.
className
string
Additional CSS classes to apply to the label.
Automatically applies error styling when the field has validation errors.

FormControl

Wrapper for the input element that provides accessibility attributes.
<FormControl>
  <Input {...field} />
</FormControl>
Automatically sets:
  • id for label association
  • aria-describedby for descriptions and errors
  • aria-invalid when there are errors

FormDescription

Helper text displayed below the input field.
className
string
Additional CSS classes to apply to the description.
<FormDescription>
  Enter your email address to receive notifications.
</FormDescription>

FormMessage

Displays validation error messages.
reserveSpace
boolean
default:"true"
When true, reserves space for error messages even when empty to prevent layout shift.
className
string
Additional CSS classes to apply to the message container.
Features:
  • Displays error icon when there’s an error
  • Supports multiple error messages
  • Deduplicates identical error messages
  • Can reserve space to prevent layout shift

Hook

useFormField

Access form field context within a form component.
import { useFormField } from '@repo/ui/form'

function CustomFormComponent() {
  const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
  
  return (
    // Use form field data
  )
}
Returns:
  • id - Unique field ID
  • name - Field name
  • formItemId - ID for the form item
  • formDescriptionId - ID for the description
  • formMessageId - ID for error messages
  • error - Current field error
  • invalid - Whether the field is invalid
  • isDirty - Whether the field has been modified
  • isTouched - Whether the field has been focused

Examples

Complete Form with Validation

import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'
import { Form, FormField, FormItem, FormLabel, FormControl, FormMessage } from '@repo/ui/form'
import { Input } from '@repo/ui/input'
import { Button } from '@repo/ui/button'

const formSchema = z.object({
  email: z.string().email('Please enter a valid email address'),
  password: z.string().min(8, 'Password must be at least 8 characters'),
})

export default function LoginForm() {
  const form = useForm<z.infer<typeof formSchema>>({
    resolver: zodResolver(formSchema),
    defaultValues: {
      email: '',
      password: '',
    },
  })

  const onSubmit = async (data: z.infer<typeof formSchema>) => {
    // Handle form submission
    console.log(data)
  }

  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
        <FormField
          control={form.control}
          name="email"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Email</FormLabel>
              <FormControl>
                <Input {...field} type="email" placeholder="[email protected]" />
              </FormControl>
              <FormMessage />
            </FormItem>
          )}
        />
        
        <FormField
          control={form.control}
          name="password"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Password</FormLabel>
              <FormControl>
                <Input {...field} type="password" placeholder="••••••••" />
              </FormControl>
              <FormMessage />
            </FormItem>
          )}
        />
        
        <Button type="submit" full>
          Sign In
        </Button>
      </form>
    </Form>
  )
}

Newsletter Subscription Form

import { useState } from 'react'
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'
import { Form, FormField, FormControl, FormMessage } from '@repo/ui/form'
import { Input } from '@repo/ui/input'
import { Button } from '@repo/ui/button'

const formSchema = z.object({
  email: z.string().email(),
})

export default function Subscribe() {
  const [isPending, setIsPending] = useState(false)
  
  const form = useForm<z.infer<typeof formSchema>>({
    resolver: zodResolver(formSchema),
    defaultValues: {
      email: '',
    },
  })

  const onSubmit = async ({ email }: z.infer<typeof formSchema>) => {
    setIsPending(true)
    // Handle subscription
    setIsPending(false)
  }

  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(onSubmit)}>
        <FormField
          control={form.control}
          name="email"
          render={({ field }) => (
            <>
              <FormControl>
                <Input {...field} type="email" placeholder="Email" />
              </FormControl>
              <FormMessage />
            </>
          )}
        />
        <Button type="submit" loading={isPending}>
          Subscribe
        </Button>
      </form>
    </Form>
  )
}

Features

  • Type-safe: Full TypeScript support with type inference
  • Validation: Built-in integration with Zod schemas
  • Accessible: Proper ARIA attributes and label associations
  • Error Handling: Automatic error display with icons
  • Layout Control: Optional space reservation to prevent layout shift

Accessibility

  • Proper label-input association via htmlFor and id
  • Error messages linked via aria-describedby
  • Invalid state indicated via aria-invalid
  • Screen reader friendly error announcements
  • Info icon to visually indicate errors

Source

View source: packages/ui/src/form/form.tsx

Build docs developers (and LLMs) love