# TanStack Form (/docs/tanstack-form)



TanStack Form owns the form state, and Luke UI renders the controls.

## Initialise the form [#initialise-the-form]

Call `useForm` with `defaultValues` and a submit handler. `revalidateLogic` holds validation back
until the first submit, then revalidates each field as it changes.

```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: '' },
	onSubmit: ({ value }) => saveAccount(value),
	validationLogic: revalidateLogic({ mode: 'submit', modeAfterSubmission: 'change' }),
	validators: { onDynamic: schema, onSubmit: schema },
});
```

TanStack Form accepts any Standard Schema validator, so Zod needs no resolver package. The form's
value types come from `defaultValues`, and TypeScript checks the schema against them.

## Integrate components [#integrate-components]

Give `form.Field` a `name` and a children function. Read the value from `field.state.value`, pass
`field.handleChange` to `onChange`, and pass `field.handleBlur` to `onBlur`.

apps/docs/src/examples/forms/tanstack-form.tsx

```tsx
import { Box } from '@luke-ui/react/box';
import { Button } from '@luke-ui/react/button';
import { Text } from '@luke-ui/react/text';
import { TextField } from '@luke-ui/react/text-field';
import { revalidateLogic, useForm } from '@tanstack/react-form';
import { useRef } from 'react';
import * as z from 'zod';

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 FOCUSABLE_SELECTOR =
	'input:not([type="hidden"]), select, textarea, [tabindex]:not([tabindex="-1"])';

function focusFirstInvalidField(form: HTMLFormElement | null) {
	const invalid = form?.querySelector('[aria-invalid="true"]');
	if (!invalid) return;
	const control = invalid.matches(FOCUSABLE_SELECTOR)
		? invalid
		: invalid.querySelector(FOCUSABLE_SELECTOR);
	if (control instanceof HTMLElement) control.focus();
}

export default () => {
	const formRef = useRef<HTMLFormElement>(null);

	const form = useForm({
		defaultValues: { email: '', name: '' },
		onSubmit: () => undefined,
		onSubmitInvalid: () => focusFirstInvalidField(formRef.current),
		validationLogic: revalidateLogic({ mode: 'submit', modeAfterSubmission: 'change' }),
		validators: { onDynamic: schema, onSubmit: schema },
	});

	return (
		<Box display="flex" flexDirection="column" gap="sp16" maxInlineSize="20rem">
			<form
				onSubmit={(event) => {
					event.preventDefault();
					void form.handleSubmit();
				}}
				ref={formRef}
			>
				<Box display="flex" flexDirection="column" gap="sp16">
					<form.Field name="name">
						{(field) => (
							<TextField
								errorMessage={field.state.meta.errors[0]?.message}
								label="Name"
								onBlur={field.handleBlur}
								onChange={field.handleChange}
								validationBehavior="aria"
								value={field.state.value}
							/>
						)}
					</form.Field>
					<form.Field name="email">
						{(field) => (
							<TextField
								errorMessage={field.state.meta.errors[0]?.message}
								label="Email"
								onBlur={field.handleBlur}
								onChange={field.handleChange}
								validationBehavior="aria"
								value={field.state.value}
							/>
						)}
					</form.Field>
					<Box>
						<Button type="submit">Create account</Button>
					</Box>
				</Box>
			</form>
			<Text elementType="p" role="status">
				<form.Subscribe selector={(state) => (state.isSubmitSuccessful ? state.values.name : '')}>
					{(submittedName) => (submittedName ? `Submitted: ${submittedName}` : null)}
				</form.Subscribe>
			</Text>
		</Box>
	);
};
```

A checkbox reads its value from `isSelected`.

apps/docs/src/examples/forms/tanstack-form-checkbox.tsx

```tsx
import { Box } from '@luke-ui/react/box';
import { Button } from '@luke-ui/react/button';
import { Checkbox } from '@luke-ui/react/checkbox';
import { Text } from '@luke-ui/react/text';
import { revalidateLogic, useForm } from '@tanstack/react-form';
import { useRef } from 'react';
import * as z from 'zod';

const schema = z.object({
	terms: z.boolean().refine((accepted) => accepted, {
		error: 'Accept the terms of service before you continue.',
	}),
});

const FOCUSABLE_SELECTOR =
	'input:not([type="hidden"]), select, textarea, [tabindex]:not([tabindex="-1"])';

function focusFirstInvalidField(form: HTMLFormElement | null) {
	const invalid = form?.querySelector('[aria-invalid="true"]');
	if (!invalid) return;
	const control = invalid.matches(FOCUSABLE_SELECTOR)
		? invalid
		: invalid.querySelector(FOCUSABLE_SELECTOR);
	if (control instanceof HTMLElement) control.focus();
}

export default () => {
	const formRef = useRef<HTMLFormElement>(null);

	const form = useForm({
		defaultValues: { terms: false },
		onSubmit: () => undefined,
		onSubmitInvalid: () => focusFirstInvalidField(formRef.current),
		validationLogic: revalidateLogic({ mode: 'submit', modeAfterSubmission: 'change' }),
		validators: { onDynamic: schema, onSubmit: schema },
	});

	return (
		<Box display="flex" flexDirection="column" gap="sp16" maxInlineSize="20rem">
			<form
				onSubmit={(event) => {
					event.preventDefault();
					void form.handleSubmit();
				}}
				ref={formRef}
			>
				<Box display="flex" flexDirection="column" gap="sp16">
					<form.Field name="terms">
						{(field) => (
							<Checkbox
								errorMessage={field.state.meta.errors[0]?.message}
								isSelected={field.state.value}
								onBlur={field.handleBlur}
								onChange={field.handleChange}
								validationBehavior="aria"
							>
								I accept the terms of service
							</Checkbox>
						)}
					</form.Field>
					<Box>
						<Button type="submit">Continue</Button>
					</Box>
				</Box>
			</form>
			<Text elementType="p" role="status">
				<form.Subscribe selector={(state) => state.isSubmitSuccessful}>
					{(isSubmitSuccessful) => (isSubmitSuccessful ? 'Terms accepted.' : null)}
				</form.Subscribe>
			</Text>
		</Box>
	);
};
```

## Validation [#validation]

Read the message from `field.state.meta.errors` and pass it to `errorMessage`. The message marks the
field invalid.

```tsx
<form.Field name="email">
	{(field) => (
		<TextField
			errorMessage={field.state.meta.errors[0]?.message}
			label="Email"
			onBlur={field.handleBlur}
			onChange={field.handleChange}
			validationBehavior="aria"
			value={field.state.value}
		/>
	)}
</form.Field>
```

Set `validationBehavior="aria"` on every field a `form.Field` wraps. Read
[Validation](/docs/validation#let-the-browser-validate) for why native behaviour blocks the submit
event before the library can run.

## Focus the first invalid field [#focus-the-first-invalid-field]

TanStack Form leaves focus where it is after a failed submission. Hold a ref to the `<form>` element
and search it for the first control marked `aria-invalid` from `onSubmitInvalid`.

```tsx
const FOCUSABLE_SELECTOR =
	'input:not([type="hidden"]), select, textarea, [tabindex]:not([tabindex="-1"])';

function focusFirstInvalidField(form: HTMLFormElement | null) {
	const invalid = form?.querySelector('[aria-invalid="true"]');
	if (!invalid) return;
	const control = invalid.matches(FOCUSABLE_SELECTOR)
		? invalid
		: invalid.querySelector(FOCUSABLE_SELECTOR);
	if (control instanceof HTMLElement) control.focus();
}
```

`TextField`, `Checkbox`, and `ComboboxField` each mark the control itself, so the first branch
matches. The second branch covers a grouped control that marks a wrapping element instead. One ref
on the form covers every field, whatever the form grows to hold.

Reach for `inputRef` when you need a ref to one specific control. `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 a plain
`ref`.

## Submitting data [#submitting-data]

Call `form.handleSubmit()` from the form's `onSubmit`, after `event.preventDefault()`.

```tsx
<form
	onSubmit={(event) => {
		event.preventDefault();
		void form.handleSubmit();
	}}
	ref={formRef}
>
```

TanStack Form runs the schema, then calls `onSubmit` with the values or `onSubmitInvalid` with the
form API.

## Continue learning [#continue-learning]

<Cards>
  <Card href="/docs/validation" title="Validation">
    Choose validationBehavior and write the error message.
  </Card>

  <Card href="/docs/forms" title="Forms">
    Build the same form with native submit and reset.
  </Card>

  <Card href="/docs/react-hook-form" title="React Hook Form">
    See the same pattern with Controller.
  </Card>
</Cards>
