diff --git a/apps/docs/content/docs/forms.mdx b/apps/docs/content/docs/forms.mdx
index c91e31d3..97df9f74 100644
--- a/apps/docs/content/docs/forms.mdx
+++ b/apps/docs/content/docs/forms.mdx
@@ -89,4 +89,5 @@ Luke UI supports browser validation, custom validation, and server validation. T
- Pass `validationErrors` to React Aria's `Form` for server errors. Key each error by the field
`name`.
-Luke UI does not provide recipes for third-party form libraries. Start with native form behaviour.
+Start with native form behaviour. When a form library owns the form state, read
+[React Hook Form](/react-hook-form) or [TanStack Form](/tanstack-form).
diff --git a/apps/docs/content/docs/meta.json b/apps/docs/content/docs/meta.json
index f671bfdf..7bfef02b 100644
--- a/apps/docs/content/docs/meta.json
+++ b/apps/docs/content/docs/meta.json
@@ -18,6 +18,8 @@
"token-reference",
"---Guides---",
"forms",
+ "react-hook-form",
+ "tanstack-form",
"quality"
]
}
diff --git a/apps/docs/content/docs/react-hook-form.mdx b/apps/docs/content/docs/react-hook-form.mdx
new file mode 100644
index 00000000..60dfff14
--- /dev/null
+++ b/apps/docs/content/docs/react-hook-form.mdx
@@ -0,0 +1,87 @@
+---
+title: React Hook Form
+description: Wire Luke UI fields to React Hook Form with Controller.
+---
+
+React Hook Form owns the form state, and Luke UI renders the controls.
+
+## Initialise the form
+
+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.
+
+```tsx
+const schema = z.object({
+ email: z.email('Enter an email address in the form you@example.com.'),
+ name: z.string().min(1, 'Enter your name.'),
+});
+
+const form = useForm({
+ defaultValues: { email: '', name: '' },
+ resolver: zodResolver(schema),
+});
+```
+
+## Integrate components
+
+Wrap each control in `Controller`. Its `render` prop hands you `field` and `fieldState`. Pass
+`field.value`, `field.onChange`, and `field.onBlur` to the control, and give `field.ref` to
+`inputRef`.
+
+
+
+A checkbox reads its value from `isSelected`.
+
+
+
+`TextField` and `Checkbox` render a label, description, and error message around the control, so
+`inputRef` is what reaches the input underneath. A primitive that renders the control itself, such
+as `ComboboxInput`, takes `field.ref` on `ref`.
+
+## Validation
+
+Read the message from `fieldState.error` and pass `fieldState.invalid` to `isInvalid`.
+
+```tsx
+ (
+
+ )}
+/>
+```
+
+Set `validationBehavior="aria"` on every field a `Controller` wraps, so React Hook Form stays the
+only thing deciding whether the form is valid. Fields default to `validationBehavior="native"`,
+which hands each field's error state to the browser. React Aria then calls `setCustomValidity` on a
+field the resolver rejected, so the browser blocks the form before the `submit` event fires and
+pressing the button does nothing.
+
+Read [Validation](/components/forms/validation) for the other `validationBehavior` options.
+
+## Focus the first invalid field
+
+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 the person filling in
+the form gets an error message without being taken to it.
+
+Set `shouldFocusError: false` on `useForm` to turn this off.
+
+## Submitting data
+
+Wrap the submit handler in `form.handleSubmit`. It runs the schema first, then calls the handler
+with the values.
+
+```tsx
+
+ {(field) => (
+
+ )}
+
+```
+
+Set `validationBehavior="aria"` on every field a `form.Field` wraps, so TanStack Form stays the only
+thing deciding whether the form is valid. Fields default to `validationBehavior="native"`, which
+hands each field's error state to the browser. React Aria then calls `setCustomValidity` on a field
+the schema rejected, so the browser blocks the form before the `submit` event fires and pressing the
+button does nothing.
+
+Read [Validation](/components/forms/validation) for the other `validationBehavior` options.
+
+## Focus the first invalid field
+
+TanStack Form leaves focus where it is after a failed submission. Hold a ref to the `