> Leaf UI 0.6.0 · Package: @ifhover/leaf-ui · Source: b30c3d3a8fa9c5e15d0d0522cebfbd33f60c6b06
> Browser documentation: https://ifhover.github.io/leaf-ui/v/0.6.0/en/components/form.html

> For AI agents: the complete documentation index is available at https://ifhover.github.io/leaf-ui/v/0.6.0/en/llms.txt, the full documentation bundle is available at https://ifhover.github.io/leaf-ui/v/0.6.0/en/llms-full.txt.

# Form

Organize fields, labels and validation feedback, and collect form data.

## Submission and validation


*Name

*Work email

Used for project notifications

*Team

SubmitReset

basic.tsx

Copy code

```tsx lineNumbers file="../../../src/examples/en/form/basic.tsx"
import { Button, Flex, Form, FormField, Input, Select, Text } from '@ifhover/leaf-ui';

import { useState } from 'react';

export function FormBasic() {
  const [result, setResult] = useState('');
  return (
    <Form
      onSubmit={(event) => {
        event.preventDefault();
        const data = new FormData(event.currentTarget);
        setResult(`${data.get('name')} · ${data.get('email')} · ${data.get('team')}`);
      }}
      onReset={() => setResult('')}
    >
      <FormField label="Name" required>
        <Input name="name" placeholder="Your name" />
      </FormField>
      <FormField label="Work email" required help="Used for project notifications">
        <Input name="email" type="email" placeholder="hello@example.com" />
      </FormField>
      <FormField label="Team" required>
        <Select
          name="team"
          options={[
            { value: 'design', label: 'Design team' },
            { value: 'engineering', label: 'Engineering team' },
          ]}
        />
      </FormField>
      <FormField>
        <Flex gap="md" align="center" wrap justify="center">
          <Button type="submit">Submit</Button>
          <Button type="reset" variant="outline">
            Reset
          </Button>
        </Flex>
      </FormField>
      {result && (
        <output>
          <Text tone="muted">{result}</Text>
        </output>
      )}
    </Form>
  );
}
```

Expand code

## Label widths


Automatic label widthsToggle long label

Name

Work email

label-width.tsx

Copy code

```tsx lineNumbers file="../../../src/examples/en/form/label-width.tsx"
import { Button, Flex, Form, FormField, Input, Space, Switch } from '@ifhover/leaf-ui';

import { useState } from 'react';

export function FormLabelWidth() {
  const [automatic, setAutomatic] = useState(true);
  const [extra, setExtra] = useState(false);
  return (
    <Space direction="vertical" align="stretch">
      <Flex gap="md" align="center" wrap justify="center">
        <Switch checked={automatic} onChange={(event) => setAutomatic(event.target.checked)}>
          Automatic label widths
        </Switch>
        <Button variant="outline" onClick={() => setExtra(!extra)}>
          Toggle long label
        </Button>
      </Flex>
      <Form labelWidth={automatic ? 'auto' : 110}>
        <FormField label="Name">
          <Input name="name" />
        </FormField>
        <FormField label="Work email">
          <Input name="email" />
        </FormField>
        {extra && (
          <FormField label="Emergency contact number">
            <Input name="emergency" />
          </FormField>
        )}
      </Form>
    </Space>
  );
}
```

Expand code

## Extended features


ContactsAdd, remove or reorder contacts.

*Contact 1

Remove

Add contact

ValidateReset

extensions.tsx

Copy code

```tsx lineNumbers file="../../../src/examples/en/form/extensions.tsx"
import { Button, Flex, Form, FormErrorSummary, FormField, FormGroup, FormList, Input, Space } from '@ifhover/leaf-ui';

export function ExtensionDemo() {
  return (
    <Form layout="vertical" onSubmit={(e) => e.preventDefault()}>
      <FormErrorSummary />
      <FormGroup legend="Contacts" description="Add, remove or reorder contacts.">
        <FormList name="contacts" defaultValue={['']}>
          {(fields, ops) => (
            <>
              {fields.map((field) => (
                <FormField key={field.key} label={`Contact ${field.index + 1}`} required>
                  <Flex gap="sm" align="start">
                    <Input
                      style={{ flex: 1, minWidth: 0 }}
                      name={`${field.name}.email`}
                      type="email"
                      value={field.value}
                      onChange={(e) => ops.update(field.index, e.target.value)}
                    />
                    <Button variant="ghost" onClick={() => ops.remove(field.index)}>
                      Remove
                    </Button>
                    {field.index > 0 && (
                      <Button
                        variant="ghost"
                        onClick={() => ops.move(field.index, field.index - 1)}
                      >
                        Move up
                      </Button>
                    )}
                  </Flex>
                </FormField>
              ))}
              <Button variant="outline" onClick={() => ops.add('')}>
                Add contact
              </Button>
            </>
          )}
        </FormList>
      </FormGroup>
      <Space>
        <Button type="submit">Validate</Button>
        <Button type="reset" variant="outline">
          Reset
        </Button>
      </Space>
    </Form>
  );
}
```

Expand code

## Form API

| Property     | Type                         | Default        | Description                                                    |
| ------------ | ---------------------------- | -------------- | -------------------------------------------------------------- |
| `layout`     | `'horizontal' \| 'vertical'` | `'horizontal'` | Label and field layout                                         |
| `labelWidth` | `number \| 'auto'`           | `'auto'`       | A pixel width, or auto to align all labels to the widest label |
| `labelAlign` | `'left' \| 'right'`          | `'right'`      | Label alignment                                                |
| `disabled`   | `boolean`                    | `false`        | Disable interaction.                                           |
| `ref`        | `Ref<HTMLFormElement>`       | —              | Reference to the form                                          |

## FormField API

| Property     | Type        | Default   | Description                                                     |
| ------------ | ----------- | --------- | --------------------------------------------------------------- |
| `label`      | `ReactNode` | —         | Field label                                                     |
| `htmlFor`    | `string`    | Generated | The associated control ID; one field wraps one control or group |
| `required`   | `boolean`   | `false`   | Require a non-empty value; compound controls validate once.     |
| `help`       | `ReactNode` | —         | Supporting text below the control, included in the field layout |
| `error`      | `ReactNode` | —         | Error message, displayed instead of help                        |
| `labelWidth` | `number`    | —         | Override this field’s label width in pixels                     |

## Types

Import types when defining reusable component props or assembling options.

```ts
import type { FormProps, FormFieldProps } from '@ifhover/leaf-ui';
```

## Usage notes

By default, all labels in one Form use the width of its widest label. Widths update when text, fonts or mounted fields change. Vertical layouts and narrow screens stack labels above fields. Standard form attributes such as action, method, onSubmit and onReset are supported.

Help and error messages occupy normal layout space and preserve the form gap before the next field. Wrapped text naturally adds height.

Read named controls with FormData. Use required, type and minLength for basic validation; errors appear beside their fields. Pass application or server errors through error. Set noValidate when handling validation yourself. FormData contains strings, does not build nested objects, and omits unchecked checkboxes. Reset controlled fields in application state.

## Extension types

```ts
import type { FormGroupProps, FormListProps, FormListField, FormListOperations, FormErrorSummaryProps, FormError } from '@ifhover/leaf-ui';
```

## FormGroupProps API

| Property      | Type        | Default | Description            |
| ------------- | ----------- | ------- | ---------------------- |
| `legend`      | `ReactNode` | —       | Field group title      |
| `description` | `ReactNode` | —       | Supporting description |

## FormListProps API

| Property       | Type                                                                                                                                             | Default  | Description                                         |
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | -------- | --------------------------------------------------- |
| `name`         | `string`                                                                                                                                         | Required | Required. Form field name                           |
| `value`        | `readonly T[]`                                                                                                                                   | —        | Controlled value                                    |
| `defaultValue` | `readonly T[]`                                                                                                                                   | `[]`     | Initial value, restored by uncontrolled form resets |
| `onChange`     | `(values: T[]) => void`                                                                                                                          | —        | Value change callback                               |
| `itemKey`      | `(value: T) => string`                                                                                                                           | —        | Return a stable identifier                          |
| `children`     | `(fields: readonly `[`FormListField`](#formlistfield-api)`<T>[], operations: `[`FormListOperations`](#formlistoperations-api)`<T>) => ReactNode` | Required | Required. Content or child items                    |

## FormListField API

| Property | Type     | Default  | Description                               |
| -------- | -------- | -------- | ----------------------------------------- |
| `key`    | `string` | Required | Required. Unique stable identifier        |
| `index`  | `number` | Required | Required. Current index, starting at zero |
| `name`   | `string` | Required | Required. Form field name                 |
| `value`  | `T`      | Required | Required. Controlled value                |

## FormListOperations API

| Property | Type                                           | Default  | Description                                     |
| -------- | ---------------------------------------------- | -------- | ----------------------------------------------- |
| `add`    | `(value: T, index?: number) => void`           | Required | Required. Insert a field; omitted index appends |
| `remove` | `(index: number \| readonly number[]) => void` | Required | Required. Remove one or multiple indices        |
| `move`   | `(from: number, to: number) => void`           | Required | Required. Move fields preserving stable keys    |
| `update` | `(index: number, value: T) => void`            | Required | Required. Update field value                    |

## FormErrorSummaryProps API

| Property | Type                                         | Default | Description                                |
| -------- | -------------------------------------------- | ------- | ------------------------------------------ |
| `errors` | `readonly `[`FormError`](#formerror-api)`[]` | —       | Omitted collects field errors in this Form |
| `title`  | `ReactNode`                                  | —       | Title content                              |

## FormError API

| Property  | Type        | Default  | Description                                      |
| --------- | ----------- | -------- | ------------------------------------------------ |
| `fieldId` | `string`    | Required | Required. Input id focused when error is clicked |
| `message` | `ReactNode` | Required | Required. Error content                          |
| `label`   | `ReactNode` | —        | Display label                                    |

## React Hook Form integration

Install react-hook-form in your application. Use register with native inputs and Controller with components whose onChange returns a value. Leaf Form handles layout; the form library owns validation and state. Use noValidate to take over validation. FormList names are plain field names; use useFieldArray when React Hook Form manages the collection.

```tsx
import { useForm, Controller } from 'react-hook-form';
import { Form, FormField, FormErrorSummary, Input, Select, Button } from '@ifhover/leaf-ui';

export function AccountForm() {
  const { register, control, handleSubmit, formState: { errors } } = useForm({ defaultValues: { email: '', role: '' } });
  return <Form noValidate onSubmit={handleSubmit(values => console.log(values))}>
    <FormErrorSummary />
    <FormField label="Email" error={errors.email?.message}>
      <Input {...register('email', { required: 'Enter an email' })} />
    </FormField>
    <Controller name="role" control={control} rules={{ required: 'Choose a role' }} render={({ field, fieldState }) =>
      <FormField label="Role" error={fieldState.error?.message}>
        <Select {...field} options={[{ value: 'editor', label: 'Editor' }]} />
      </FormField>}
    />
    <Button type="submit">Save</Button>
  </Form>;
}
```

## FormValidationApi

| Property       | Type                                                   | Default | Description                                                         |
| -------------- | ------------------------------------------------------ | ------- | ------------------------------------------------------------------- |
| `errors`       | `Record<string, string>`                               | —       | Required. Field-keyed errors for FormField.                         |
| `pending`      | `boolean`                                              | —       | Required. Whether validation or submission is pending.              |
| `setErrors`    | `Dispatch<SetStateAction<Record<string, string>>>`     | —       | Required. Set server or business errors.                            |
| `handleSubmit` | `(event: FormEvent<HTMLFormElement>) => Promise<void>` | —       | Required. Bind to Form.onSubmit.                                    |
| `handleReset`  | `(event: FormEvent<HTMLFormElement>) => void`          | —       | Required. Bind to Form.onReset to cancel requests and clear errors. |
| `cancel`       | `() => void`                                           | —       | Required. Cancel the current request.                               |
| `clearErrors`  | `(name?: string) => void`                              | —       | Required. Clear one or all errors and cancel stale requests.        |

## FormValidationOptions

| Property     | Type                                                                                                    | Default | Description                                                                        |
| ------------ | ------------------------------------------------------------------------------------------------------- | ------- | ---------------------------------------------------------------------------------- |
| `validate`   | `( data: FormData, signal: AbortSignal, ) => Record<string, string> \| Promise<Record<string, string>>` | —       | Required. Validate FormData with a cancellation signal; return field-keyed errors. |
| `onSubmit`   | `(data: FormData, signal: AbortSignal) => void \| Promise<void>`                                        | —       | Submit after validation, respecting cancellation.                                  |
| `onError`    | `(error: unknown) => void`                                                                              | —       | Called on failure; the palette remains open with error feedback.                   |
| `focusError` | `boolean`                                                                                               | —       | Focus the first invalid field.                                                     |
