Call useForm with defaultValues and a resolver, and keep the result as form. React Hook Form
takes the form's value types from the Zod schema, so no hand-written type repeats it.
const schema = z.object({ email: z.email('Enter a valid email address.'), name: z.string().min(1, 'Enter your name.'),});const form = useForm({ defaultValues: { email: '', name: '' }, resolver: zodResolver(schema),});
Wrap the fields in Luke UI's Form. Set validationBehavior="aria" so
React Hook Form controls validation without browser submission blocking it. When every required
field should show a clear marker, set necessityIndicator="required-label".
Wrap each control in Controller. Pass field.value, field.onChange, and field.onBlur to the
control, give field.ref to inputRef, and pass field.name as the HTML name. Set isRequired
on fields the schema requires. Schema validation does not set required control semantics on its own.
Pass fieldState.error?.message to errorMessage. A non-empty message marks the field invalid.
import { zodResolver } from '@hookform/resolvers/zod';import { Button } from '@luke-ui/react/button';import { CheckboxField } from '@luke-ui/react/checkbox-field';import { Cluster } from '@luke-ui/react/cluster';import { Form } from '@luke-ui/react/form';import { Stack } from '@luke-ui/react/stack';import { Text } from '@luke-ui/react/text';import { Controller, useForm } from 'react-hook-form';import * as z from 'zod';const schema = z.object({ terms: z.boolean().refine((accepted) => accepted, { error: 'Accept the terms before you continue.', }),});export default () => { const form = useForm({ defaultValues: { terms: false }, resolver: zodResolver(schema), }); return ( <Stack gap="space.16" style={{ maxInlineSize: '20rem' }}> <Form necessityIndicator="required-label" onSubmit={form.handleSubmit(() => undefined)} validationBehavior="aria" > <Stack gap="space.16"> <Controller control={form.control} name="terms" render={({ field, fieldState }) => { return ( <CheckboxField errorMessage={fieldState.error?.message} inputRef={field.ref} isRequired isSelected={field.value} label="I accept the terms of service" name={field.name} onBlur={field.onBlur} onChange={field.onChange} /> ); }} /> <Cluster> <Button type="submit">Continue</Button> </Cluster> </Stack> </Form> <Text elementType="p" isVisuallyHidden={!form.formState.isSubmitSuccessful} role="status"> {form.formState.isSubmitSuccessful ? 'Terms accepted.' : null} </Text> </Stack> );};
TextInputField, CheckboxField, and SwitchField render a label, description, and error message
around the control, so inputRef is what reaches the input underneath. For SelectField, pass the
library's ref through triggerRef to reach the trigger button. A primitive that renders the control
itself, such as ComboboxInput, takes field.ref on ref.
Read Validation for controlled errors versus native validation. Read
Required fields for necessity indicators.
React Hook Form focuses the first invalid control after a failed submission, using the ref each
field registered. A field that never receives field.ref stays unfocused, and someone filling in
the form gets an error message without being taken to it.
Set shouldFocusError: false on useForm to turn this off.
Pass form.handleSubmit(onSubmit) to Form's onSubmit. React Hook Form validates the fields before
calling your handler. Prefer onSubmit over Form's action for this integration, because the
library needs the submit event.