On this page

Guides

Forms

Build an accessible form from Luke UI's Form and field components, and handle submit and reset.

A Luke UI form uses Form. Compose it from Luke UI fields and buttons. Put fields in a layout primitive such as Stack. Form is not a layout wrapper.

Forms: Build a form
We will send the receipt to this address.

import { Button } from '@luke-ui/react/button';
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 { TextInputField } from '@luke-ui/react/text-input-field';
import { useActionState } from 'react';

export default () => {
	const [submittedEmail, formAction, isPending] = useActionState(submitEmail, '');

	return (
		<Stack gap="space.16" style={{ maxInlineSize: '20rem' }}>
			<Form action={formAction} necessityIndicator="required-label">
				<Stack gap="space.16">
					<TextInputField
						description="We will send the receipt to this address."
						isRequired
						label="Email address"
						name="email"
						type="email"
					/>
					<Cluster gap="space.8">
						<Button isPending={isPending} type="submit">
							Send receipt
						</Button>
					</Cluster>
				</Stack>
			</Form>
			<Text elementType="p" isVisuallyHidden={!submittedEmail} role="status">
				{submittedEmail ? `Receipt sent to ${submittedEmail}.` : null}
			</Text>
		</Stack>
	);
};

async function submitEmail(_previous: string, formData: FormData) {
	const email = formData.get('email');
	return typeof email === 'string' ? email : '';
}

Visible labels

Pass a visible label to each field or radio group. Luke UI associates the label with its control or group. A label takes textual content only. Place a link or button next to the field instead.

When a component supports a name without a visible label, pass aria-labelledby when content already on the page supplies the name, or aria-label to supply the name directly. Check the component's API for the names it accepts. Checkbox, switch, and radio options always take a visible label. When your layout draws an option's label separately, compose the Checkbox primitive or Switch primitive, or the Radio primitive.

A placeholder disappears after someone enters a value. Do not use a placeholder as the only label.

Descriptions

Pass description when someone needs information before they complete a field. Luke UI associates the description with its control.

Keep each description to one sentence. Luke UI keeps it visible and associated with the control when an error message appears.

Required fields

Requiredness has three parts:

  • Semantics. Set isRequired on each field that must be completed. With native validation, the browser blocks submission while a required field is empty. When validationBehavior="aria" is set, the control remains required, but the application or form library handles validation and submission.
  • Visible marker. Choose how the label shows that state with necessityIndicator.
  • Accessible name. Required markers stay visual. Assistive technology reads required state from the control. An optional marker with 'optional-label' becomes part of the accessible name.

Choose a marker with necessityIndicator:

  • 'required-asterisk' adds a visual * after a required label.
  • 'required-label' adds visible "(required)" after a required label.
  • 'optional-label' adds "(optional)" after a definitely optional label.

Set necessityIndicator on Form to use the same convention across its fields. An individual field can override that setting. When neither the field nor its Form specifies an indicator, required labels display an asterisk.

A field with only aria-label or aria-labelledby shows no marker and gets no "(optional)" suffix.

When required fields use an asterisk, explain what the marker means near the start of the form. When using 'optional-label', explain that fields are required unless marked optional. 'required-label' states requiredness in the label itself, so a separate legend is usually unnecessary. The indicator changes the label, not the field's required state.

Group related controls with a native <fieldset> and <legend> when they share one question or topic, such as a set of notification options. The legend names the group.

Forms: Related fields
Email notifications
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 type { SubmitEvent } from 'react';

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

export default () => {
	return (
		<Form onSubmit={handleSubmit}>
			<Stack gap="space.16" style={{ maxInlineSize: '20rem' }}>
				<fieldset style={{ border: 'none', margin: 0, minInlineSize: 0, padding: 0 }}>
					<legend>Email notifications</legend>
					<Stack gap="space.12" paddingBlockStart="space.12">
						<CheckboxField defaultSelected label="Product updates" name="productUpdates" />
						<CheckboxField defaultSelected label="Security alerts" name="securityAlerts" />
						<CheckboxField label="Tips and tutorials" name="tips" />
					</Stack>
				</fieldset>
				<Cluster>
					<Button type="submit">Save preferences</Button>
				</Cluster>
			</Stack>
		</Form>
	);
};

Controlled and uncontrolled fields

Prefer an uncontrolled field when the application needs its value only during submission. Pass a starting value through defaultValue or defaultSelected.

Use a controlled field when the application needs its value during each render. Pass value and onChange, then store the value in state.

For a controlled CheckboxField or SwitchField, pass isSelected and onChange.

Submit

Prefer Form's action for ordinary submissions. Pass a React Action, often from useActionState, so the handler receives FormData without a submit event. Use the pending flag for the submit Button. After a successful Action, React resets uncontrolled fields. Return server errors from the Action and pass them through validationErrors. Read Server validation for that pattern.

Use onSubmit when the handler needs the submit event: calling preventDefault, reading the form element, or wiring a form library. Libraries such as React Hook Form and TanStack Form expect onSubmit. They own pending state and errors themselves. Read React Hook Form and TanStack Form for those integrations.

React's form Actions document action and pending behaviour in detail.

Give the submit button type="submit".

Reset

Avoid a type="reset" button in ordinary forms. Someone can clear completed work by accident.

When a form needs to restore defaults, a reset control can be appropriate. The browser restores uncontrolled fields to their starting values. Controlled fields keep the state React owns, so an onReset handler must reset that state too.

That is separate from the automatic reset after a successful Action, which restores uncontrolled fields without a reset button.

Fields outside the form element

A field outside the form element can participate in native submission through the HTML form attribute. That association does not establish React ancestry. If the field is outside Form's React subtree, it does not inherit the shared necessity indicator, validation behaviour, or server errors. Set necessityIndicator and validationBehavior on the field itself, and pass server errors through its errorMessage.

Validation

Start with native form validation. Read Validation for browser constraints, custom rules, server errors, and focus after an unsuccessful submission. When a form library owns the form state, read React Hook Form or TanStack Form.

Continue learning