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, and this page is available as Markdown at https://ifhover.github.io/leaf-ui/v/0.6.0/en/components/form.md.
  • English
  • Form

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

    Submission and validation

    Used for project notifications
    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>
      );
    }
    

    Label widths

    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>
      );
    }
    

    Extended features

    Contacts

    Add, remove or reorder contacts.

    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>
      );
    }
    

    Form API

    PropertyTypeDefaultDescription
    layout'horizontal' | 'vertical''horizontal'Label and field layout
    labelWidthnumber | 'auto''auto'A pixel width, or auto to align all labels to the widest label
    labelAlign'left' | 'right''right'Label alignment
    disabledbooleanfalseDisable interaction.
    refRef<HTMLFormElement>—Reference to the form

    FormField API

    PropertyTypeDefaultDescription
    labelReactNode—Field label
    htmlForstringGeneratedThe associated control ID; one field wraps one control or group
    requiredbooleanfalseRequire a non-empty value; compound controls validate once.
    helpReactNode—Supporting text below the control, included in the field layout
    errorReactNode—Error message, displayed instead of help
    labelWidthnumber—Override this field’s label width in pixels

    Types

    Import types when defining reusable component props or assembling options.

    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

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

    FormGroupProps API

    PropertyTypeDefaultDescription
    legendReactNode—Field group title
    descriptionReactNode—Supporting description

    FormListProps API

    PropertyTypeDefaultDescription
    namestringRequiredRequired. Form field name
    valuereadonly T[]—Controlled value
    defaultValuereadonly 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<T>[], operations: FormListOperations<T>) => ReactNodeRequiredRequired. Content or child items

    FormListField API

    PropertyTypeDefaultDescription
    keystringRequiredRequired. Unique stable identifier
    indexnumberRequiredRequired. Current index, starting at zero
    namestringRequiredRequired. Form field name
    valueTRequiredRequired. Controlled value

    FormListOperations API

    PropertyTypeDefaultDescription
    add(value: T, index?: number) => voidRequiredRequired. Insert a field; omitted index appends
    remove(index: number | readonly number[]) => voidRequiredRequired. Remove one or multiple indices
    move(from: number, to: number) => voidRequiredRequired. Move fields preserving stable keys
    update(index: number, value: T) => voidRequiredRequired. Update field value

    FormErrorSummaryProps API

    PropertyTypeDefaultDescription
    errorsreadonly FormError[]—Omitted collects field errors in this Form
    titleReactNode—Title content

    FormError API

    PropertyTypeDefaultDescription
    fieldIdstringRequiredRequired. Input id focused when error is clicked
    messageReactNodeRequiredRequired. Error content
    labelReactNode—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.

    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

    PropertyTypeDefaultDescription
    errorsRecord<string, string>—Required. Field-keyed errors for FormField.
    pendingboolean—Required. Whether validation or submission is pending.
    setErrorsDispatch<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

    PropertyTypeDefaultDescription
    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.
    focusErrorboolean—Focus the first invalid field.