Skip to content
GitHub

React Hook Form

Build forms in React using React Hook Form and Zod.

This guide shows how to pair React Hook Form with Shark UI’s form primitives. Shark UI’s Field components are built on Ark UI, so they work naturally with React Hook Form while keeping validation, error handling, and accessibility in sync. For additional patterns (including native controls with register and custom components with Controller).

Demo

The example below is a bug report form: a text input for the title and a textarea for the description. On submit, the form is validated with Zod and any errors are shown alongside the relevant fields.

Note: Browser validation is disabled in this demo to illustrate schema validation. In production, keep native validation enabled when appropriate.

Approach

The pattern uses React Hook Form for state and Zod for validation. Shark UI’s Ark-based Field primitives handle layout and a11y, leaving you full control over markup and styling.

  • Form state: React Hook Form’s useForm and Controller for controlled fields
  • Layout & semantics: Shark UI Field, FieldLabel, FieldError, and related components
  • Validation: Zod schema with zodResolver for client-side validation

Anatomy

Typical structure: wrap each field with Controller, and use Shark UI’s Field for layout and error wiring.

Form

Create a form schema

Define your form shape with a Zod schema.

Note: React Hook Form supports other Standard Schema libraries; Zod is used here for clarity.

Setup the form

Create the form with useForm and pass zodResolver(formSchema) so Zod runs validation.

Done

With that, the form is ready: accessible labels and errors come from the Field components, and Zod runs on submit. Invalid submissions populate fieldState.error so FieldError can render messages.

Validation

Client-side validation

Use a Zod schema as the resolver in useForm. Validation runs according to the mode you set.

Validation modes

Displaying errors

Use FieldError and pass invalid down so both the Field wrapper and the control (e.g. Input, SelectTrigger, Checkbox) get the correct states for styling and ARIA.

Working with different field types

Simple inputs with register

For plain text inputs and textareas, you can use register instead of Controller—fewer wrappers and less boilerplate.

Input with Controller

Use Controller when you need controlled behavior (e.g. character counters) or when integrating custom components. Spread field onto Input and pass invalid={fieldState.invalid} to both Field and Input.

Textarea

Same idea as Input: spread field onto Textarea or InputGroupTextarea, and pass invalid to both the Field and the textarea.

NativeSelect

Use NativeSelect for a native HTML <select>. Like plain inputs, use register instead of Controller—no controlled wrappers needed.

Select

Use Select for a custom dropdown with a collection. Wire value and onValueChange; for single select, value is string[] with one item. Forward the field ref to SelectTrigger so React Hook Form can focus the first invalid field on submit.

Checkbox

For checkbox lists, manage an array in field.value and use field.onChange. Set data-slot="checkbox-group" on FieldGroup for correct spacing.

Radio group

Wire field.value and field.onChange to RadioGroup; pass invalid to both Field and RadioGroupItem.

Switch

Use field.value and field.onChange with Switch; pass invalid to both Field and the switch.

NumberField

Use field.value and field.onChange with NumberField; the onValueChange callback provides { value }, which you pass to field.onChange. Pass invalid to both Field and NumberField.

Slider

Use field.value (as number[]) and field.onChange with Slider; the onValueChange callback provides { value }. Single-thumb sliders use [50], so field.value is the first element.

Segment Group

Use field.value and field.onChange with SegmentGroup; pass invalid to Field. Wire value and onValueChange to the root.

Combobox

Use field.value and field.onChange with Combobox; the onValueChange callback provides the selected value. Forward the field’s ref to the trigger so React Hook Form can focus invalid fields on submit.

Date Picker

Use field.value and field.onChange with DatePicker; the value is a DateValue from @internationalized/date. Convert to/from ISO string for storage if needed.

Input OTP

Use field.value (as string[]) and field.onChange with InputOtp; the onValueChange callback provides { value }. Join the array for submission (e.g. value.join("")).

Color Picker

Use field.value (hex string) and field.onChange with ColorPicker; wire value and onValueChange. Use ColorPickerInput for an inline input, or ColorPickerTrigger + popover for a picker UI.

Rating

Use field.value (number) and field.onChange with Rating; the onValueChange callback provides { value }.

File Upload

FileUpload uses a native hidden input, so it participates in form submission. Use Controller to read the file list from the input’s onChange, or use register if you only need the native behavior.

Resetting the form

Call form.reset() to restore default values.

Array fields

Use useFieldArray when you have dynamic lists of fields (e.g. multiple emails). It exposes fields, append, and remove.

Using useFieldArray

Array field structure

Group array items in a FieldSet with FieldLegend and FieldDescription.

Controller pattern for array items

Map over fields and wrap each item in a Controller. Use field.id as the React key.

Adding items

Call append(newItem) to push a new entry.

Removing items

Call remove(index). Typically you only show the remove action when there’s more than one item.

Array validation

Validate arrays with Zod’s z.array() and .min() / .max().