# Chakra UI

> This document is the Markdown version of [formisch.dev/react/guides/chakra-ui/](https://formisch.dev/react/guides/chakra-ui/). For the complete documentation index, see [llms.txt](https://formisch.dev/llms.txt).

[Chakra UI](https://chakra-ui.com) is a component system for building accessible React applications. This guide targets Chakra UI v3, whose composition-based API differs fundamentally from v2. No adapter package is needed: native controls receive `field.props`, while the Ark UI based composite components map Formisch's value and lifecycle APIs to their parts.

## Installation

Install Formisch, Valibot and Chakra UI, and follow the [Chakra UI installation guide](https://chakra-ui.com/docs/get-started/installation) to set up the provider:

```bash
npm install @formisch/react valibot @chakra-ui/react @emotion/react
```

## Wiring patterns

Choose the wiring from the component's public API:

- If it forwards props and a ref to a native form element, spread `field.props` and pass `field.input` as its value.
- If it exposes a details callback such as `onCheckedChange` or `onValueChange`, control it with `field.input` and `field.onChange`, then forward the remaining lifecycle props to the component's native parts.

### Spreading field.props

`Input` and `Textarea` render native elements, so their wiring is the same as a plain HTML input. Chakra's `Field.Root` wires the label, error and `aria` attributes to the nested input automatically. Since both Formisch and Chakra UI export a component named `Field`, the Chakra one is imported as `ChakraField`:

```tsx
<Field of={loginForm} path={['email']}>
  {(field) => (
    <ChakraField.Root invalid={field.errors !== null}>
      <ChakraField.Label>Email</ChakraField.Label>
      <Input {...field.props} value={field.input ?? ''} type="email" />
      <ChakraField.ErrorText>{field.errors?.[0]}</ChakraField.ErrorText>
    </ChakraField.Root>
  )}
</Field>
```

### Controlled components

Chakra's composite components expose their state through a details object and render a hidden native element for form integration. Control the value on the root, and pass the field name, ref and lifecycle props to the hidden native part:

```tsx
<Field of={loginForm} path={['rememberMe']}>
  {(field) => (
    <Checkbox.Root
      checked={field.input ?? false}
      onCheckedChange={(details) => field.onChange(!!details.checked)}
    >
      <Checkbox.HiddenInput
        name={field.props.name}
        ref={field.props.ref}
        autoFocus={field.props.autoFocus}
        onFocus={field.props.onFocus}
        onBlur={field.props.onBlur}
      />
      <Checkbox.Control>
        <Checkbox.Indicator />
      </Checkbox.Control>
      <Checkbox.Label>Remember me</Checkbox.Label>
    </Checkbox.Root>
  )}
</Field>
```

The hidden input is the component's real focus target, so registering it with `field.props.ref` keeps Formisch's `focus()` working. The `details.checked` value can be `'indeterminate'`, so `!!details.checked` normalizes it to a boolean. Formisch's focus and blur handlers are parameterless because they only update field lifecycle state, so you can pass them directly even when the component supplies an event argument.

These examples wire fields directly so you can see the complete integration. Once a pattern repeats, wrap the parts in your own input component. The [input components guide](/react/guides/input-components.md) shows how.

## Displaying errors

Formisch exposes errors as `[string, ...string[]] | null`. Wrap any control in `ChakraField.Root` and pass its `invalid` prop; `ChakraField.ErrorText` then renders the message, and nested native inputs receive `aria-invalid` and `aria-describedby` automatically:

```tsx
<Field of={loginForm} path={['email']}>
  {(field) => (
    <ChakraField.Root invalid={field.errors !== null}>
      <ChakraField.Label>Email</ChakraField.Label>
      <Input {...field.props} value={field.input ?? ''} />
      <ChakraField.ErrorText>{field.errors?.[0]}</ChakraField.ErrorText>
    </ChakraField.Root>
  )}
</Field>
```

Composite components such as `Select.Root` and `Slider.Root` additionally accept their own `invalid` prop to mark the visible trigger. By default, Formisch validates on submit and revalidates on input. The [validation guide](/react/guides/validation.md) explains how to change that timing.

## Login form example

This complete example combines native inputs, a controlled checkbox and accessible errors:

```tsx
import {
  Button,
  Field as ChakraField,
  Checkbox,
  Input,
} from '@chakra-ui/react';
import { Field, Form, type SubmitHandler, useForm } from '@formisch/react';
import * as v from 'valibot';

const LoginSchema = v.object({
  email: v.pipe(
    v.string(),
    v.nonEmpty('Please enter your email.'),
    v.email('The email address is badly formatted.')
  ),
  password: v.pipe(
    v.string(),
    v.nonEmpty('Please enter your password.'),
    v.minLength(8, 'Your password must have 8 characters or more.')
  ),
  rememberMe: v.optional(v.boolean(), false),
});

export default function LoginPage() {
  const loginForm = useForm({
    schema: LoginSchema,
    initialInput: {
      email: '',
      password: '',
      rememberMe: false,
    },
  });

  const handleSubmit: SubmitHandler<typeof LoginSchema> = (output) => {
    console.log(output);
  };

  return (
    <Form of={loginForm} onSubmit={handleSubmit}>
      <Field of={loginForm} path={['email']}>
        {(field) => (
          <ChakraField.Root invalid={field.errors !== null}>
            <ChakraField.Label>Email</ChakraField.Label>
            <Input
              {...field.props}
              value={field.input ?? ''}
              type="email"
              placeholder="jane@example.com"
              autoComplete="email"
            />
            <ChakraField.ErrorText>{field.errors?.[0]}</ChakraField.ErrorText>
          </ChakraField.Root>
        )}
      </Field>

      <Field of={loginForm} path={['password']}>
        {(field) => (
          <ChakraField.Root invalid={field.errors !== null}>
            <ChakraField.Label>Password</ChakraField.Label>
            <Input
              {...field.props}
              value={field.input ?? ''}
              type="password"
              autoComplete="current-password"
            />
            <ChakraField.ErrorText>{field.errors?.[0]}</ChakraField.ErrorText>
          </ChakraField.Root>
        )}
      </Field>

      <Field of={loginForm} path={['rememberMe']}>
        {(field) => (
          <Checkbox.Root
            checked={field.input ?? false}
            onCheckedChange={(details) => field.onChange(!!details.checked)}
          >
            <Checkbox.HiddenInput
              name={field.props.name}
              ref={field.props.ref}
              autoFocus={field.props.autoFocus}
              onFocus={field.props.onFocus}
              onBlur={field.props.onBlur}
            />
            <Checkbox.Control>
              <Checkbox.Indicator />
            </Checkbox.Control>
            <Checkbox.Label>Remember me</Checkbox.Label>
          </Checkbox.Root>
        )}
      </Field>

      <Button type="submit" disabled={loginForm.isSubmitting}>
        Login
      </Button>
    </Form>
  );
}
```

The initial input keeps every control defined from its first render. The native inputs use the `field.props` spread, while the checkbox maps Chakra's details callback and hidden input explicitly.

## Component reference

The following snippets assume a form store named `form` and initialized values that match the schema.

### Text input

`Input` forwards to a native `<input>`, so spread `field.props` directly:

```tsx
<Field of={form} path={['email']}>
  {(field) => (
    <ChakraField.Root invalid={field.errors !== null}>
      <ChakraField.Label>Email</ChakraField.Label>
      <Input {...field.props} value={field.input ?? ''} type="email" />
      <ChakraField.ErrorText>{field.errors?.[0]}</ChakraField.ErrorText>
    </ChakraField.Root>
  )}
</Field>
```

`Textarea` works the same way because it also renders a native element.

### Checkbox

For a boolean field, control `checked` on the root and register the hidden input:

```tsx
<Field of={form} path={['newsletter']}>
  {(field) => (
    <Checkbox.Root
      checked={field.input ?? false}
      onCheckedChange={(details) => field.onChange(!!details.checked)}
    >
      <Checkbox.HiddenInput
        name={field.props.name}
        ref={field.props.ref}
        autoFocus={field.props.autoFocus}
        onFocus={field.props.onFocus}
        onBlur={field.props.onBlur}
      />
      <Checkbox.Control>
        <Checkbox.Indicator />
      </Checkbox.Control>
      <Checkbox.Label>Newsletter</Checkbox.Label>
    </Checkbox.Root>
  )}
</Field>
```

### Select

`Select` works with a collection created by `createListCollection` and represents its value as a `string[]`. Convert between the array and Formisch's single value, and narrow the plain string back to the schema union. The hidden select redirects focus to the visible trigger, so registering it with `field.props.ref` keeps `focus()` working:

```tsx
const frameworks = createListCollection({
  items: [
    { label: 'Angular', value: 'angular' },
    { label: 'React', value: 'react' },
    { label: 'Solid', value: 'solid' },
    { label: 'Vue', value: 'vue' },
  ],
});
```

```tsx
<Field of={form} path={['framework']}>
  {(field) => (
    <ChakraField.Root invalid={field.errors !== null}>
      <Select.Root
        collection={frameworks}
        invalid={field.errors !== null}
        value={field.input ? [field.input] : []}
        onValueChange={(details) =>
          field.onChange((details.value[0] ?? undefined) as typeof field.input)
        }
      >
        <Select.HiddenSelect name={field.props.name} ref={field.props.ref} />
        <Select.Label>Framework</Select.Label>
        <Select.Control>
          <Select.Trigger
            autoFocus={field.props.autoFocus}
            onFocus={field.props.onFocus}
            onBlur={field.props.onBlur}
          >
            <Select.ValueText placeholder="Select a framework" />
          </Select.Trigger>
          <Select.IndicatorGroup>
            <Select.Indicator />
          </Select.IndicatorGroup>
        </Select.Control>
        <Portal>
          <Select.Positioner>
            <Select.Content>
              {frameworks.items.map((item) => (
                <Select.Item item={item} key={item.value}>
                  {item.label}
                  <Select.ItemIndicator />
                </Select.Item>
              ))}
            </Select.Content>
          </Select.Positioner>
        </Portal>
      </Select.Root>
      <ChakraField.ErrorText>{field.errors?.[0]}</ChakraField.ErrorText>
    </ChakraField.Root>
  )}
</Field>
```

### Radio group

`RadioGroup` is controlled through its details callback, and each item renders a hidden native radio input. Register the first item's hidden input so Formisch can move focus into the group:

```tsx
<Field of={form} path={['plan']}>
  {(field) => (
    <ChakraField.Root invalid={field.errors !== null}>
      <ChakraField.Label>Plan</ChakraField.Label>
      <RadioGroup.Root
        name={field.props.name}
        value={field.input ?? null}
        onValueChange={(details) =>
          field.onChange((details.value ?? undefined) as typeof field.input)
        }
      >
        <RadioGroup.Item value="hobby">
          <RadioGroup.ItemHiddenInput
            ref={field.props.ref}
            autoFocus={field.props.autoFocus}
            onFocus={field.props.onFocus}
            onBlur={field.props.onBlur}
          />
          <RadioGroup.ItemIndicator />
          <RadioGroup.ItemText>Hobby</RadioGroup.ItemText>
        </RadioGroup.Item>
        <RadioGroup.Item value="pro">
          <RadioGroup.ItemHiddenInput />
          <RadioGroup.ItemIndicator />
          <RadioGroup.ItemText>Pro</RadioGroup.ItemText>
        </RadioGroup.Item>
      </RadioGroup.Root>
      <ChakraField.ErrorText>{field.errors?.[0]}</ChakraField.ErrorText>
    </ChakraField.Root>
  )}
</Field>
```

### Slider

Pass `field.input` wrapped in a one-element array for a single thumb and unwrap the details value in the callback. `Slider.Label` gives the slider its accessible name, and the lifecycle handlers go on the thumb, which is the keyboard focus target.

Chakra's slider keeps its focusable thumb as a `div` and renders its hidden input with `display: none`, so there is no element Formisch can register and focus. That means `focus` and submit-time error focusing cannot reach this field, while its value, validation and blur handling work normally:

```tsx
<Field of={form} path={['volume']}>
  {(field) => (
    <ChakraField.Root invalid={field.errors !== null}>
      <Slider.Root
        invalid={field.errors !== null}
        value={[field.input ?? 50]}
        onValueChange={(details) => field.onChange(details.value[0] ?? 50)}
        min={0}
        max={100}
      >
        <Slider.Label>Volume</Slider.Label>
        <Slider.Control>
          <Slider.Track>
            <Slider.Range />
          </Slider.Track>
          <Slider.Thumb
            index={0}
            onFocus={field.props.onFocus}
            onBlur={field.props.onBlur}
          >
            <Slider.HiddenInput name={field.props.name} />
          </Slider.Thumb>
        </Slider.Control>
      </Slider.Root>
      <ChakraField.ErrorText>{field.errors?.[0]}</ChakraField.ErrorText>
    </ChakraField.Root>
  )}
</Field>
```

## Library-specific notes

This guide covers Chakra UI v3 only. The v2 API (`FormControl`, `isInvalid`, plain `onChange` callbacks) is entirely different; consult the [Chakra migration guide](https://chakra-ui.com/docs/get-started/migration) if you are upgrading.

## Next steps

Read the [input components](/react/guides/input-components.md) guide to package repeated wiring into reusable controls, the [controlled fields](/react/guides/controlled-fields.md) guide for the underlying pattern, and the [validation](/react/guides/validation.md) guide for validation timing.
