Ark UI Logo
Guides
Forms

Forms

A guide to building forms with Ark UI components.

Ark UI provides the Field and Fieldset components for integrating with native form element or popular form libraries like React Hook Form, TanStack Form, and Vee Validate.

Field Context

Form components in Ark UI automatically integrate with Field through context. When nested inside a Field.Root, they inherit disabled, invalid, required, and readOnly states automatically.

import { Field } from '@ark-ui/react/field'
import { NumberInput } from '@ark-ui/react/number-input'

const Demo = () => (
  <Field.Root disabled>
    <NumberInput.Root /> {/* NumberInput will be disabled */}
  </Field.Root>
)

Accessible Labels

When building accessible forms, you need to ensure that they are properly labeled and described.

  • Field.Label: Used to provide an accessible label the input.
  • Field.HelperText: Used to provide additional instructions about the input.

These components are automatically linked to the input element via the aria-describedby attribute.

Best practice: Make sure that labels are visible (and not just used as placeholders) for screen readers to read them.

import { Field } from '@ark-ui/react/field'

const Demo = () => (
  <form>
    <Field.Root>
      <Field.Label>Username</Field.Label>
      <Field.Input placeholder="Enter your username" />
      <Field.HelperText>This will be your public display name.</Field.HelperText>
    </Field.Root>
  </form>
)

Error Handling and Validation

When the input is invalid, you can use the Field.ErrorText component to provide an error message for the input, and pass the invalid prop to the Field.Root component.

Best practice: Make sure to provide clear, specific error messages that are easy to understand and fix.

import { Field } from '@ark-ui/react/field'

const Demo = () => (
  <form>
    <Field.Root invalid>
      <Field.Label>Username</Field.Label>
      <Field.Input placeholder="Enter your username" />
      <Field.ErrorText>Username is required.</Field.ErrorText>
    </Field.Root>
  </form>
)

Required Fields

To indicate that a field is required, you can pass the required prop to the Field.Root component. Optionally, you can use the Field.RequiredIndicator component to indicate that the field is required.

Best practice: Don't rely solely on color to indicate required status

import { Field } from '@ark-ui/react/field'

export const Demo = () => (
  <form>
    <Field.Root required>
      <Field.Label>
        Username
        <Field.RequiredIndicator>(required)</Field.RequiredIndicator>
      </Field.Label>
      <Field.Input placeholder="Enter your username" />
      <Field.ErrorText>Username is required.</Field.ErrorText>
    </Field.Root>
  </form>
)

To indicate that a field is optional, use the fallback prop on the Field.RequiredIndicator component.

<Field.RequiredIndicator fallback="Optional">(required)</Field.RequiredIndicator>

Native Controls

Field supports native HTML form controls including input, textarea, and select:

import { Field } from '@ark-ui/react/field'

const Demo = () => (
  <form>
    {/* Input */}
    <Field.Root>
      <Field.Label>Email</Field.Label>
      <Field.Input type="email" placeholder="you@example.com" />
    </Field.Root>

    {/* Textarea */}
    <Field.Root>
      <Field.Label>Bio</Field.Label>
      <Field.Textarea placeholder="Tell us about yourself" />
    </Field.Root>

    {/* Select */}
    <Field.Root>
      <Field.Label>Country</Field.Label>
      <Field.Select>
        <option value="us">United States</option>
        <option value="uk">United Kingdom</option>
        <option value="de">Germany</option>
      </Field.Select>
    </Field.Root>
  </form>
)

Form Reset

When the reset event is triggered on a form, all Ark UI components automatically sync their internal state with the form's reset values.

Note: For this to work correctly, always include the HiddenInput component in your form controls. The hidden input participates in the native form reset mechanism, and Ark UI listens for this to sync the component state.

import { Checkbox } from '@ark-ui/react/checkbox'

const Demo = () => {
  return (
    <form>
      <Checkbox.Root name="terms" defaultChecked>
        <Checkbox.Label>I agree to the terms</Checkbox.Label>
        <Checkbox.Control>
          <Checkbox.Indicator />
        </Checkbox.Control>
        <Checkbox.HiddenInput />
      </Checkbox.Root>

      {/* Clicking reset will restore checkbox to defaultChecked state */}
      <button type="reset">Reset</button>
      <button type="submit">Submit</button>
    </form>
  )
}

Fieldset Context

When you have multiple fields in a form or a component that renders multiple input elements, you can use the Fieldset component to group them together.

Common use cases checkbox group, radio group, input + select composition, etc.

Checkbox Group

import { Fieldset } from '@ark-ui/react/fieldset'
import { Checkbox } from '@ark-ui/react/checkbox'
import { CheckIcon } from 'lucide-react'

const items = [
  { label: 'React', value: 'react' },
  { label: 'Solid', value: 'solid' },
  { label: 'Vue', value: 'vue' },
  { label: 'Svelte', value: 'svelte' },
]

const Demo = () => (
  <Fieldset.Root>
    <Fieldset.Legend>Frameworks</Fieldset.Legend>
    <Checkbox.Group name="framework">
      {items.map((item) => (
        <Checkbox.Root value={item.value} key={item.value} />
      ))}
    </Checkbox.Group>
    <Fieldset.HelperText>Choose your preferred frameworks</Fieldset.HelperText>
  </Fieldset.Root>
)

Radio Group

import { Fieldset } from '@ark-ui/react/fieldset'
import { RadioGroup } from '@ark-ui/react/radio-group'
import { RadioIcon } from 'lucide-react'

const items = [
  { label: 'React', value: 'react' },
  { label: 'Solid', value: 'solid' },
  { label: 'Vue', value: 'vue' },
  { label: 'Svelte', value: 'svelte' },
]

const Demo = () => (
  <Fieldset.Root>
    <Fieldset.Legend>Frameworks</Fieldset.Legend>
    <RadioGroup.Root name="framework">
      {items.map((item) => (
        <RadioGroup.Item value={item.value} key={item.value} />
      ))}
    </RadioGroup.Root>
    <Fieldset.HelperText>Choose your preferred framework</Fieldset.HelperText>
  </Fieldset.Root>
)

React Hook Form

Ark UI integrates seamlessly with React Hook Form. Use the register function for simple inputs and Controller for complex components.

Native Controls

import { Field } from '@ark-ui/react/field'
import { useForm } from 'react-hook-form'

interface FormValues {
  firstName: string
  email: string
}

const Demo = () => {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm<FormValues>()

  const onSubmit = (data: FormValues) => console.log(data)

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <Field.Root invalid={!!errors.firstName}>
        <Field.Label>First Name</Field.Label>
        <Field.Input {...register('firstName', { required: 'First name is required' })} />
        <Field.ErrorText>{errors.firstName?.message}</Field.ErrorText>
      </Field.Root>

      <Field.Root invalid={!!errors.email}>
        <Field.Label>Email</Field.Label>
        <Field.Input
          {...register('email', {
            required: 'Email is required',
            pattern: { value: /^\S+@\S+$/i, message: 'Invalid email address' },
          })}
        />
        <Field.ErrorText>{errors.email?.message}</Field.ErrorText>
      </Field.Root>

      <button type="submit">Submit</button>
    </form>
  )
}

Custom Components

Use the Controller hook to integrate custom form components like select or combobox.

Best practice: To ensure react-hook-form moves focus to invalid field control when performing validation, forward the field's ref to the respective Ark UI component.

import { Field } from '@ark-ui/react/field'
import { Select, createListCollection } from '@ark-ui/react/select'
import { Controller, useForm } from 'react-hook-form'

const Demo = () => {
  const { control, handleSubmit } = useForm({
    defaultValues: { framework: '' },
  })

  const collection = createListCollection({
    items: ['React', 'Solid', 'Vue', 'Svelte'],
  })

  return (
    <form onSubmit={handleSubmit((data) => console.log(data))}>
      <Controller
        name="framework"
        control={control}
        rules={{ required: 'Please select a framework' }}
        render={({
          field: { name, ref, value, onBlur, onChange },
          fieldState: { invalid, isTouched, isDirty, error },
        }) => (
          <Field.Root invalid={invalid}>
            <Select.Root
              name={name}
              collection={collection}
              value={value ? [value] : []}
              onValueChange={(e) => onChange(e.value[0])}
              onInteractOutside={() => onBlur()}
            >
              <Select.Label>Framework</Select.Label>
              <Select.Control>
                <Select.Trigger ref={ref}>
                  <Select.ValueText placeholder="Select a Framework" />
                </Select.Trigger>
              </Select.Control>
              <Select.Positioner>
                <Select.Content>
                  {collection.items.map((item) => (
                    <Select.Item key={item} item={item}>
                      <Select.ItemText>{item}</Select.ItemText>
                    </Select.Item>
                  ))}
                </Select.Content>
              </Select.Positioner>
              <Select.HiddenSelect />
            </Select.Root>
            <Field.ErrorText>{error?.message}</Field.ErrorText>
          </Field.Root>
        )}
      />
      <button type="submit">Submit</button>
    </form>
  )
}

TanStack Form

TanStack Form provides powerful form state management that works well with Ark UI.

Native Controls

import { useForm } from '@tanstack/react-form'

const form = useForm({
  defaultValues: { firstName: '' },
})

const Demo = () => (
  <form onSubmit={form.handleSubmit}>
    <form.Field
      name="firstName"
      children={(field) => (
        <Field.Root
          name={field.name}
          invalid={!field.state.meta.isValid}
          dirty={field.state.meta.isDirty}
          touched={field.state.meta.isTouched}
        >
          <Field.Label>First Name</Field.Label>
          <Field.Input
            value={field.state.value}
            onChange={(e) => field.handleChange(e.target.value)}
            onBlur={field.handleBlur}
          />
          <Field.ErrorText match={!field.state.meta.isValid}>{field.state.meta.errors.join(',')}</Field.ErrorText>
        </Field.Root>
      )}
    />
    <button type="submit">Submit</button>
  </form>
)

Custom Components

Here's an example of how to integrate custom components like the select or combobox with Tanstack's form.Field component.

import { Field } from '@ark-ui/react/field'
import { Select, createListCollection } from '@ark-ui/react/select'
import { useForm } from '@tanstack/react-form'

const Demo = () => {
  const form = useForm({
    defaultValues: { username: '' },
  })

  const collection = createListCollection({
    items: ['React', 'Solid', 'Vue', 'Svelte'],
  })

  return (
    <form
      onSubmit={(e) => {
        e.preventDefault()
        e.stopPropagation()
        form.handleSubmit()
      }}
    >
      <form.Field
        name="username"
        validators={{
          onChange: ({ value }) => (value.length < 3 ? 'Username must be at least 3 characters' : undefined),
        }}
        children={(field) => (
          <Field.Root invalid={field.state.meta.errors.length > 0}>
            <Field.Label>Username</Field.Label>
            <Select.Root
              name={field.name}
              collection={collection}
              value={field.state.value ? [field.state.value] : []}
              onValueChange={(e) => field.handleChange(e.value[0])}
              onInteractOutside={() => field.handleBlur()}
            >
              <Select.Label>Username</Select.Label>
              <Select.Control>
                <Select.Trigger>
                  <Select.ValueText placeholder="Enter your username" />
                </Select.Trigger>
              </Select.Control>
              <Select.Positioner>
                <Select.Content>
                  {collection.items.map((item) => (
                    <Select.Item key={item} item={item}>
                      <Select.ItemText>{item}</Select.ItemText>
                    </Select.Item>
                  ))}
                </Select.Content>
              </Select.Positioner>
              <Select.HiddenSelect />
            </Select.Root>
          </Field.Root>
        )}
      />
      <button type="submit">Submit</button>
    </form>
  )
}