Form
Building forms with React Hook Form and Zod.
'use client'
import Button from '@dinui/react/button'import Form, { useForm } from '@dinui/react/form'import Input from '@dinui/react/input'import { zodResolver } from '@hookform/resolvers/zod'import { z } from 'zod'
const FormSchema = z.object({ username: z.string().min(2, { message: 'Username must be at least 2 characters.', }),})
export default function InputForm() { const form = useForm<z.infer<typeof FormSchema>>({ resolver: zodResolver(FormSchema), defaultValues: { username: '', }, })
function onSubmit(data: z.infer<typeof FormSchema>) { alert(`You submitted the following values: ${JSON.stringify(data, null, 2)}`) }
return ( <Form form={form} onSubmit={form.handleSubmit(onSubmit)} className="w-2/3 space-y-6"> <Form.Field control={form.control} name="username" render={({ field }) => ( <Form.Item> <Form.Label>Username</Form.Label> <Form.Control> <Input placeholder="dinwwwh" {...field} /> </Form.Control> <Form.Description>This is your public display name.</Form.Description> <Form.ErrorMessage /> </Form.Item> )} />
<Button type="submit">Submit</Button> </Form> )}Forms are tricky. They are one of the most common things you’ll build in a web application, but also one of the most complex.
Well-designed HTML forms are:
- Well-structured and semantically correct.
- Easy to use and navigate (keyboard).
- Accessible with ARIA attributes and proper labels.
- Has support for client and server side validation.
- Well-styled and consistent with the rest of the application.
In this guide, we will take a look at building forms with react-hook-form and zod.
We’re going to use a <Form> component to compose accessible forms using Radix UI components.
Features
The <Form /> component is a wrapper around the react-hook-form library. It provides a few things:
- Composable components for building forms.
- A
<Form.Field />component for building controlled form fields. - Form validation using
zod. - Handles accessibility and error messages.
- Uses
useId()for generating unique IDs. - Applies the correct
ariaattributes to form fields based on states. - Built to work with all Radix UI components.
- Bring your own schema library. We use
zodbut you can use anything you want. - You have full control over the markup and styling.
Examples
See the following links for more examples on how to use the <Form /> component with other components:
Installation
-
Follow Installation Guide
To enable DinUI functionality in your project, you will need to properly set up Tailwind and install the necessary dependencies. -
All done
You now can start using this component in your project.
-
Follow Installation Guide
To enable DinUI functionality in your project, you will need to properly set up Tailwind and install the necessary dependencies. -
Run the following command in your project
Terminal window npx @dinui/cli@latest add formTerminal window yarn dlx @dinui/cli@latest add formTerminal window pnpm dlx @dinui/cli@latest add formTerminal window bunx @dinui/cli@latest add form -
Update the import paths to match your project setup
-
All done
You now can start using this component in your project.
-
Follow Installation Guide
To enable DinUI functionality in your project, you will need to properly set up Tailwind and install the necessary dependencies. -
Install dependencies
Terminal window npm install @radix-ui/react-slot react-hook-form react-hook-form tailwind-variants type-fest react-hook-form react-hook-formTerminal window yarn add @radix-ui/react-slot react-hook-form react-hook-form tailwind-variants type-fest react-hook-form react-hook-formTerminal window pnpm add @radix-ui/react-slot react-hook-form react-hook-form tailwind-variants type-fest react-hook-form react-hook-formTerminal window bun add @radix-ui/react-slot react-hook-form react-hook-form tailwind-variants type-fest react-hook-form react-hook-form -
Copy and paste the following code into your project
import Label from '@dinui/react/label'import { Slot } from '@radix-ui/react-slot'import { createContext, forwardRef, useContext, useId } from 'react'import type { ControllerProps, FieldPath, FieldValues, UseFormReturn } from 'react-hook-form'import { Controller, FormProvider, useFormContext } from 'react-hook-form'import { tv } from 'tailwind-variants'import type { Merge } from 'type-fest'const form = tv({slots: {item: 'flex flex-col gap-2',description: 'text-sm text-fg-weaker',errorMessage: 'text-sm font-medium text-fg-danger',},})function FormRoot({form,asChild,...props}: Merge<React.ComponentProps<'form'>,{// eslint-disable-next-line @typescript-eslint/no-explicit-anyform: UseFormReturn<any>asChild?: boolean}>) {const Comp = asChild ? Slot : 'form'return (<FormProvider {...form}><Comp {...props} /></FormProvider>)}const FormFieldContext = createContext<{ name: FieldPath<FieldValues> } | undefined>(undefined)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 FormItemContext = createContext<{ id: string } | undefined>(undefined)const FormItem = forwardRef<HTMLDivElement,Merge<React.HTMLAttributes<HTMLDivElement>,{asChild?: boolean}>>(({ asChild, ...props }, ref) => {const id = useId()const { item } = form()const Comp = asChild ? Slot : 'div'return (<FormItemContext.Provider value={{ id }}><Comp {...props} ref={ref} className={item({ className: props.className })} /></FormItemContext.Provider>)})FormItem.displayName = 'FormItem'const useFormField = () => {const fieldContext = useContext(FormFieldContext)const itemContext = useContext(FormItemContext)const { getFieldState, formState } = useFormContext()if (!fieldContext) {throw new Error('useFormField should be used within <Form.Field>')}if (!itemContext) {throw new Error('useFormField should be used within <Form.Item>')}const fieldState = getFieldState(fieldContext.name, formState)const { id } = itemContextreturn {...fieldState,id,name: fieldContext.name,formItemId: `${id}-form-item`,formDescriptionId: `${id}-form-item-description`,formMessageId: `${id}-form-item-message`,}}const FormLabel = forwardRef<React.ElementRef<typeof Label>,React.ComponentPropsWithoutRef<typeof Label>>((props, ref) => {const { error, formItemId } = useFormField()return <Label {...props} ref={ref} variant={error ? 'danger' : 'default'} htmlFor={formItemId} />})FormLabel.displayName = 'FormLabel'const FormControl = forwardRef<React.ElementRef<typeof Slot>,React.ComponentPropsWithoutRef<typeof Slot>>(({ ...props }, ref) => {const { error, formItemId, formDescriptionId, formMessageId } = useFormField()return (<Slotref={ref}id={formItemId}aria-describedby={!error ? `${formDescriptionId}` : `${formDescriptionId} ${formMessageId}`}aria-invalid={!!error}{...props}/>)})FormControl.displayName = 'FormControl'const FormDescription = forwardRef<HTMLParagraphElement,Merge<React.HTMLAttributes<HTMLParagraphElement>,{asChild?: boolean}>>(({ asChild, ...props }, ref) => {const { formDescriptionId } = useFormField()const { description } = form()const Comp = asChild ? Slot : 'p'return (<Comp{...props}ref={ref}id={formDescriptionId}className={description({ className: props.className })}/>)})FormDescription.displayName = 'FormDescription'const FormErrorMessage = forwardRef<HTMLParagraphElement,Merge<React.HTMLAttributes<HTMLParagraphElement>,{asChild?: boolean}>>(({ asChild, children, ...props }, ref) => {const { error, formMessageId } = useFormField()const body = error ? String(error?.message) : childrenconst { errorMessage } = form()if (!body) {return null}const Comp = asChild ? Slot : 'p'return (<Comp{...props}ref={ref}id={formMessageId}className={errorMessage({ className: props.className })}>{body}</Comp>)})FormErrorMessage.displayName = 'FormErrorMessage'const Form = Object.assign(FormRoot, {Field: FormField,Item: FormItem,Label: FormLabel,Control: FormControl,Description: FormDescription,ErrorMessage: FormErrorMessage,})export default Formexport { form }export { useForm } from 'react-hook-form'export * as FormPrimitive from 'react-hook-form' -
Update the import paths to match your project setup
-
All done
You now can start using this component in your project.