# Validation (/docs/validation)



Every Luke UI form field can report a problem. An invalid field takes the danger border colour and
renders a message beneath the control.

apps/docs/src/examples/forms/validation.tsx

```tsx
import { Box } from '@luke-ui/react/box';
import { Button } from '@luke-ui/react/button';
import { Checkbox } from '@luke-ui/react/checkbox';
import { ComboboxField } from '@luke-ui/react/combobox-field';
import { ComboboxItem } from '@luke-ui/react/primitives/combobox';
import { TextField } from '@luke-ui/react/text-field';
import type { SubmitEvent } from 'react';
import { useState } from 'react';

const countries = [
	{ id: 'australia', label: 'Australia' },
	{ id: 'canada', label: 'Canada' },
	{ id: 'new-zealand', label: 'New Zealand' },
	{ id: 'united-states', label: 'United States' },
];

type Errors = { country?: string; email?: string; terms?: string };

export default () => {
	const [errors, setErrors] = useState<Errors>({});

	function handleSubmit(event: SubmitEvent<HTMLFormElement>) {
		event.preventDefault();
		const data = new FormData(event.currentTarget);
		const emailValue = data.get('emailAddress');
		const email = typeof emailValue === 'string' ? emailValue : '';

		setErrors({
			country: data.get('country') ? undefined : 'Choose the country where you work.',
			email: email.includes('@')
				? undefined
				: 'Enter an email address in the form you@example.com.',
			terms: data.get('terms') ? undefined : 'Accept the terms of service before you continue.',
		});
	}

	return (
		<form noValidate onSubmit={handleSubmit}>
			<Box display="flex" flexDirection="column" gap="sp16" maxInlineSize="20rem">
				<TextField
					errorMessage={errors.email}
					label="Email address"
					name="emailAddress"
					placeholder="you@example.com"
				/>
				<ComboboxField
					defaultItems={countries}
					errorMessage={errors.country}
					label="Work location"
					name="country"
					placeholder="Choose a country"
				>
					{(item) => <ComboboxItem>{item.label}</ComboboxItem>}
				</ComboboxField>
				<Checkbox errorMessage={errors.terms} name="terms">
					I accept the terms of service
				</Checkbox>
				<Box>
					<Button type="submit">Create account</Button>
				</Box>
			</Box>
		</form>
	);
};
```

`errorMessage` means one thing: this field currently has a controlled error. Pass it when you
already have a message from a form library, your server, or your own state. The field is invalid as
soon as you pass a non-empty message. When the field should generate its own message instead, from a
constraint or a `validate` rule, leave `errorMessage` out entirely.

## Let the browser validate [#let-the-browser-validate]

Set `isRequired` on `TextField`, `ComboboxField`, or `Checkbox` to require a value before the form
submits. Pass `minLength`, `maxLength`, or `pattern` on `TextField` to state a rule for the text.
Pass `type="email"` or `type="url"` to check the value's format.

Leave out `errorMessage`. The field renders the message the constraint produces. It appears on blur
or on submit.

apps/docs/src/examples/forms/constraint-validation.tsx

```tsx
import { Box } from '@luke-ui/react/box';
import { Button } from '@luke-ui/react/button';
import { TextField } from '@luke-ui/react/text-field';

export default () => {
	return (
		<form>
			<Box display="flex" flexDirection="column" gap="sp16" maxInlineSize="20rem">
				<TextField isRequired label="Email address" name="emailAddress" type="email" />
				<Box>
					<Button type="submit">Create account</Button>
				</Box>
			</Box>
		</form>
	);
};
```

Pass `validationBehavior="native"` to block submission until every field passes. This is the
default.

Pass `validationBehavior="aria"` to mark an invalid field for assistive technology. The form still
submits. Set this on every field a form library wraps, so the library stays the only thing deciding
whether the form is valid. Native behaviour calls `setCustomValidity` on a rejected field, so the
browser blocks the submit event and the library never runs.

## Custom validation [#custom-validation]

Pass a `validate` function for a rule the browser cannot check, such as a reserved username. The
function takes the value and returns the validation message, or `null` when the value is valid.
Leave out `errorMessage`. The field renders the message `validate` returns.

apps/docs/src/examples/forms/custom-validation.tsx

```tsx
import { Box } from '@luke-ui/react/box';
import { Button } from '@luke-ui/react/button';
import { TextField } from '@luke-ui/react/text-field';
import type { SubmitEvent } from 'react';

const reservedUsernames = new Set(['admin', 'root', 'support']);

function validateUsername(value: string): string | null {
	if (reservedUsernames.has(value.trim().toLowerCase())) {
		return 'That username is reserved. Choose another.';
	}

	return null;
}

export default () => {
	function handleSubmit(event: SubmitEvent<HTMLFormElement>) {
		event.preventDefault();
	}

	return (
		<form onSubmit={handleSubmit}>
			<Box display="flex" flexDirection="column" gap="sp16" maxInlineSize="20rem">
				<TextField
					defaultValue="admin"
					label="Username"
					name="username"
					validate={validateUsername}
				/>
				<Box>
					<Button type="submit">Create account</Button>
				</Box>
			</Box>
		</form>
	);
};
```

Return one actionable message, even when several conditions could fail. Check the most important
condition first so the next message appears once that one is fixed. You can return an array of
messages, but the field does not render them as a list. It joins them with a space, so they read as
a single run-on sentence, and even two short messages can fill two lines in a 288px-wide form
column.

With the default validation behaviour, the field shows the validation message when someone commits
the value, for example on blur or submit, rather than between keystrokes.

## Realtime validation [#realtime-validation]

Pass a controlled `errorMessage` for feedback that must update on every keystroke, such as password
strength. You decide validity here, not the field. Make the field controlled with `value` and
`onChange`, and compute `errorMessage` from that value on every render. An absent message keeps the
field valid, so `undefined`, `null`, `false`, and an empty string all mean no error.

```tsx
const [password, setPassword] = useState('');
const isTooShort = password.length > 0 && password.length < 12;

<TextField
	errorMessage={isTooShort ? 'Passwords need at least 12 characters.' : undefined}
	label="Password"
	onChange={setPassword}
	type="password"
	value={password}
/>;
```

This is the exception. Constraint and `validate` messages normally appear after the value is
committed. This does not distract someone while they type.

## Form libraries [#form-libraries]

Pass the library's current error message as `errorMessage`. The field goes invalid as soon as the
library reports one, and clears once the library clears it. Read
[React Hook Form](/docs/react-hook-form) and [TanStack Form](/docs/tanstack-form) for the full
setup, including `validationBehavior="aria"` so the library stays the only thing deciding whether
the form is valid.

## Server validation [#server-validation]

Pass server errors through `Form` from `react-aria-components`. `@luke-ui/react` does not export
`Form`, so import it separately.

```tsx
import { Form } from 'react-aria-components';

<Form validationErrors={{ username: 'That username is taken.' }}>
	<TextField label="Username" name="username" />
</Form>;
```

Pass `validationErrors` with an object keyed by each field's `name`. Each field shows its message as
soon as you pass `validationErrors`. The message clears once someone changes the field's value.

## When errors appear and clear [#when-errors-appear-and-clear]

An error appears on blur or on submit. It never appears between keystrokes. An error clears as soon
as the value changes.

The controlled `errorMessage` pattern in the realtime validation section is the exception. It
updates on every keystroke because that is its purpose.

## Always pair an invalid field with a message [#always-pair-an-invalid-field-with-a-message]

Colour alone cannot report a problem. Someone who cannot separate the danger border from the resting
one gets no signal at all. An invalid field with no message is invisible to them.

`TextField` and `ComboboxField` add an error icon inside the control, after the value. The icon says
that something is wrong, not what is wrong, so it does not stand in for the message.

Every invalid field needs a message so the error is not conveyed by colour alone, which is what WCAG
1.4.1 Use of Colour requires. Let a constraint or `validate` generate the message, or pass your own
through `errorMessage`. The primitives take `isInvalid` directly, so pair that state with a message
yourself.

## Write the message [#write-the-message]

An error message has two jobs: name the problem, and name the fix.

`errorMessage` takes any React content, so a message with a link or emphasis works directly.

| Instead of             | Write                                                                         |
| ---------------------- | ----------------------------------------------------------------------------- |
| Invalid input          | Enter an email address in the form [you@example.com](mailto:you@example.com). |
| This field is required | Choose the country where you work.                                            |
| Error                  | Passwords need at least 12 characters.                                        |

* Say what to do next, not only what went wrong. "Choose a delivery date" beats "No date selected".
* Use the field's own words. If the label says "Work location", the message says work location, not
  region.
* Keep it to one sentence. A long message pushes the rest of the form down the page.
* Describe what is wrong with the value. Do not blame the reader. "Enter a date after today" rather
  than "You entered an invalid date".
* Repeat anything someone needs from `description`. They may never look back up.

## Continue learning [#continue-learning]

<Cards>
  <Card href="/docs/forms" title="Forms">
    Build a native form from Luke UI fields.
  </Card>

  <Card href="/docs/react-hook-form" title="React Hook Form">
    Hand validity to React Hook Form with validationBehavior.
  </Card>

  <Card href="/docs/tanstack-form" title="TanStack Form">
    Hand validity to TanStack Form with validationBehavior.
  </Card>

  <Card href="/components/forms/text-field" title="Text Field">
    See labels, required markers, and error messages on a field.
  </Card>
</Cards>
