rapidUI

Form

react-hook-form bindings (`Form`, `FormField`, `FormItem`, `FormMessage`) with accessible labels and errors.

Installation

pnpm dlx shadcn@latest add https://rdf.dev.360online.com/r/form.json

Installs to @/components/ui/form.

Manual installation

Install the dependencies:

pnpm add @base-ui/react@^1.8.0 react-hook-form@^7.62.0

Add the components it builds on: label.

Copy the source into your project:

components/ui/form.tsx
'use client'

import { mergeProps } from '@base-ui/react/merge-props'
import { useRender } from '@base-ui/react/use-render'
import * as React from 'react'
import {
	Controller,
	type ControllerProps,
	type FieldPath,
	type FieldValues,
	FormProvider,
	useFormContext,
	useFormState,
} from 'react-hook-form'
import { Label } from '@/components/ui/label'
import { cn } from '@/lib/utils'

const Form = FormProvider

type FormFieldContextValue<
	TFieldValues extends FieldValues = FieldValues,
	TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> = {
	name: TName
}

const FormFieldContext = React.createContext<FormFieldContextValue>({} as FormFieldContextValue)

const FormField = <
	TFieldValues extends FieldValues = FieldValues,
	TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>({
	...props
}: ControllerProps<TFieldValues, TName>) => {
	return (
		<FormFieldContext.Provider value={{ name: props.name }}>
			<Controller {...props} />
		</FormFieldContext.Provider>
	)
}

const useFormField = () => {
	const fieldContext = React.useContext(FormFieldContext)
	const itemContext = React.useContext(FormItemContext)
	const { getFieldState } = useFormContext()
	const formState = useFormState({ name: fieldContext.name })
	const fieldState = getFieldState(fieldContext.name, formState)

	if (!fieldContext) {
		throw new Error('useFormField should be used within <FormField>')
	}

	const { id } = itemContext

	return {
		id,
		name: fieldContext.name,
		formItemId: `${id}-form-item`,
		formDescriptionId: `${id}-form-item-description`,
		formMessageId: `${id}-form-item-message`,
		...fieldState,
	}
}

type FormItemContextValue = {
	id: string
}

const FormItemContext = React.createContext<FormItemContextValue>({} as FormItemContextValue)

function FormItem({ className, ...props }: React.ComponentProps<'div'>) {
	const id = React.useId()

	return (
		<FormItemContext.Provider value={{ id }}>
			<div data-slot="form-item" className={cn('grid gap-2', className)} {...props} />
		</FormItemContext.Provider>
	)
}

function FormLabel({ className, ...props }: React.ComponentProps<typeof Label>) {
	const { error, formItemId } = useFormField()

	return (
		<Label
			data-slot="form-label"
			data-error={!!error}
			className={cn('data-[error=true]:text-destructive break-words', className)}
			htmlFor={formItemId}
			{...props}
		/>
	)
}

type FormControlProps = React.HTMLAttributes<HTMLElement> & {
	ref?: React.Ref<HTMLElement>
}

// Passes the field's id, aria-describedby and aria-invalid on to its only child (the form control)
function FormControl({ children, ref, ...props }: FormControlProps) {
	const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
	const child = React.isValidElement(children) ? children : undefined

	return useRender({
		defaultTagName: 'div',
		render: child,
		ref,
		props: mergeProps<'div'>(
			{
				'data-slot': 'form-control',
				id: formItemId,
				'aria-describedby': error ? `${formDescriptionId} ${formMessageId}` : `${formDescriptionId}`,
				'aria-invalid': !!error,
				children: child ? undefined : children,
			} as React.ComponentProps<'div'>,
			props as React.ComponentProps<'div'>,
		),
	})
}

function FormDescription({ className, ...props }: React.ComponentProps<'p'>) {
	const { formDescriptionId } = useFormField()

	return (
		<p
			data-slot="form-description"
			id={formDescriptionId}
			className={cn('text-muted-foreground text-sm break-words', className)}
			{...props}
		/>
	)
}

function FormMessage({ className, ...props }: React.ComponentProps<'p'>) {
	const { error, formMessageId } = useFormField()
	const body = error ? String(error?.message ?? '') : props.children

	if (!body) {
		return null
	}

	return (
		<p data-slot="form-message" id={formMessageId} className={cn('text-destructive text-sm', className)} {...props}>
			{body}
		</p>
	)
}

export { useFormField, Form, FormItem, FormLabel, FormControl, FormDescription, FormMessage, FormField }

A form component built on top of React Hook Form with validation using Zod. The form components provide a flexible and accessible way to build forms with validation, error messages, and complex form state management.

Features

  • Built on React Hook Form for powerful form state management
  • Zod schema validation integration
  • Accessible form controls with ARIA attributes
  • Error message handling
  • Form field descriptions
  • Flexible form layouts
  • TypeScript support
  • Keyboard navigation
  • Screen reader support

Usage

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

// Define your form schema
const formSchema = z.object({
  caseNumber: z.string().regex(/^\d{4}-\d{5}$/, {
    message: 'Case numbers look like 2026-04812.',
  }),
})

export function FindCaseForm() {
  // Initialize form with schema
  const form = useForm<z.infer<typeof formSchema>>({
    resolver: zodResolver(formSchema),
    defaultValues: {
      caseNumber: '',
    },
  })

  // Form submission handler
  function onSubmit(values: z.infer<typeof formSchema>) {
    console.log(values)
  }

  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
        <FormField
          control={form.control}
          name="caseNumber"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Case number</FormLabel>
              <FormControl>
                <Input placeholder="2026-04812" {...field} />
              </FormControl>
              <FormDescription>
                You find it in the confirmation email.
              </FormDescription>
              <FormMessage />
            </FormItem>
          )}
        />
        <Button type="submit">Find case</Button>
      </form>
    </Form>
  )
}

Examples

School placement

A card-based e-service form with a text input, a select, a date, a switch and a required confirmation checkbox. Submit it empty to see the Zod messages appear under each field.

Public records request

Radio options are composed from nested FormItems so each option gets its own label and hint, while the group shares one FormMessage.

Validation errors

The form is validated on mount (form.trigger()), showing how FormLabel, FormControl and FormMessage pick up the error state and wire aria-invalid and aria-describedby for you.

Form Components

Form

The root form component that provides form context.

Props:

  • All props from react-hook-form's FormProvider

FormField

A component that provides form field context and handles field registration.

Props:

  • control - Form control from React Hook Form
  • name - Field name
  • render - Render function for the field

FormItem

Container for form field components.

Props:

  • className - Additional CSS classes
  • Standard HTML div props

FormLabel

Label component for form fields.

Props:

  • Standard HTML label props

FormControl

Wraps a single form control and passes the field's id, aria-describedby and aria-invalid on to it (merged with Base UI's useRender, so the control renders without an extra element).

Props:

  • Standard HTML attributes, merged onto the child
  • Handles aria-describedby and aria-invalid automatically

FormDescription

Descriptive text for form fields.

Props:

  • className - Additional CSS classes
  • Standard HTML paragraph props

FormMessage

Component for displaying validation errors.

Props:

  • className - Additional CSS classes
  • Standard HTML paragraph props

Accessibility

The Form components follow WAI-ARIA guidelines:

  • Uses semantic HTML elements
  • Proper label associations with form controls
  • Error messages are announced to screen readers
  • Proper ARIA attributes for validation states
  • Keyboard navigation support
  • Focus management
  • Clear error indication

Best Practices

  1. Form Structure:

    • Group related fields
    • Use clear labels
    • Provide helpful descriptions
    • Show validation errors inline
  2. Validation:

    • Use Zod schemas for validation
    • Provide clear error messages
    • Validate in real-time when appropriate
    • Handle all possible states
  3. User Experience:

    • Clear submission feedback
    • Disable submit while processing
    • Preserve form state when appropriate
    • Handle errors gracefully
  4. Accessibility:

    • Maintain proper heading hierarchy
    • Use semantic HTML
    • Test with screen readers
    • Ensure keyboard navigation
  • Input
  • Select
  • Checkbox
  • RadioGroup
  • Switch
  • Textarea