Integration / UI Libraries

Integrating with UI libraries

In this guide, we will show you how to integrate custom inputs with Conform.

#Event delegation

Conform supports all native inputs out of the box by attaching an input and focusout event listener on the document directly. There is no need to setup any event handlers on the <input />, <select /> or <textarea /> elements. The only requirement is to set a form id on the <form /> element and make sure all the inputs have a name attribute set and are associated with the form either by using the form attribute or by nesting them inside the <form /> element.

1function Example() {
2  const [form, fields] = useForm({
3    // Optional, Conform will generate a random id if not provided
4    id: 'example',
5  });
6
7  return (
8    <form id={form.id}>
9      <div>
10        <label>Title</label>
11        <input type="text" name="title" />
12        <div>{fields.title.errors}</div>
13      </div>
14      <div>
15        <label>Description</label>
16        <textarea name="description" />
17        <div>{fields.description.errors}</div>
18      </div>
19      <div>
20        <label>Color</label>
21        <select name="color">
22          <option>Red</option>
23          <option>Green</option>
24          <option>Blue</option>
25        </select>
26        <div>{fields.color.errors}</div>
27      </div>
28      <button form={form.id}>Submit</button>
29    </form>
30  );
31}

#Identifying if integration is needed

Conform relies on event delegation to validate the form and will work with any custom input as long as it dispatches the form events. This is usually true for simple components that are just a wrapper around the native input element like <Input /> or <Textarea />. However, custom inputs such as <Select /> or <DatePicker /> will likely require users to interact with non native form element with a hidden input and so no form event would be dispatched.

To identify if the input is a native input, you can wrap it in a div with event listeners attached to see if the form events are dispatched and bubble up while you are interacting with the custom input. You will also find examples below for some of the popular UI libraries.

1import { CustomInput } from 'your-ui-library';
2
3function Example() {
4  return (
5    <div onInput={console.log} onBlur={console.log}>
6      <CustomInput />
7    </div>
8  );
9}

#Enhancing custom inputs with useInputControl

To fix this issue, Conform provides a hook called useInputControl that let you enhance a custom input so that it dispatch the form events when needed. The hook returns a control object with the following properties:

  • value: The current value of the input with respect to form reset and update intents
  • change: A function to update the current value and dispatch both change and input events
  • focus: A function to dispatch focus and focusin events
  • blur: A function to dispatch blur and focusout events

Here is an example wrapping the Select component from Radix UI:

1import {
2  type FieldMetadata,
3  useForm,
4  useInputControl,
5} from '@conform-to/react';
6import * as Select from '@radix-ui/react-select';
7import {
8  CheckIcon,
9  ChevronDownIcon,
10  ChevronUpIcon,
11} from '@radix-ui/react-icons';
12
13type SelectFieldProps = {
14  // You can use the `FieldMetadata` type to define the `meta` prop
15  // And restrict the type of the field it accepts through its generics
16  meta: FieldMetadata<string>;
17  options: Array<string>;
18};
19
20function SelectField({ meta, options }: SelectFieldProps) {
21  const control = useInputControl(meta);
22
23  return (
24    <Select.Root
25      name={meta.name}
26      value={control.value}
27      onValueChange={(value) => {
28        control.change(value);
29      }}
30      onOpenChange={(open) => {
31        if (!open) {
32          control.blur();
33        }
34      }}
35    >
36      <Select.Trigger>
37        <Select.Value />
38        <Select.Icon>
39          <ChevronDownIcon />
40        </Select.Icon>
41      </Select.Trigger>
42      <Select.Portal>
43        <Select.Content>
44          <Select.ScrollUpButton>
45            <ChevronUpIcon />
46          </Select.ScrollUpButton>
47          <Select.Viewport>
48            {options.map((option) => (
49              <Select.Item key={option} value={option}>
50                <Select.ItemText>{option}</Select.ItemText>
51                <Select.ItemIndicator>
52                  <CheckIcon />
53                </Select.ItemIndicator>
54              </Select.Item>
55            ))}
56          </Select.Viewport>
57          <Select.ScrollDownButton>
58            <ChevronDownIcon />
59          </Select.ScrollDownButton>
60        </Select.Content>
61      </Select.Portal>
62    </Select.Root>
63  );
64}
65
66function Example() {
67  const [form, fields] = useForm();
68
69  return (
70    <form id={form.id}>
71      <div>
72        <label>Currency</label>
73        <SelectField meta={fields.color} options={['red', 'green', 'blue']} />
74        <div>{fields.color.errors}</div>
75      </div>
76      <button>Submit</button>
77    </form>
78  );
79}

#Simplify it with Form Context

You can also simplify the wrapper component by using the useField hook with a FormProvider.

1import {
2  type FieldName,
3  FormProvider,
4  useForm,
5  useField,
6  useInputControl,
7} from '@conform-to/react';
8import * as Select from '@radix-ui/react-select';
9import {
10  CheckIcon,
11  ChevronDownIcon,
12  ChevronUpIcon,
13} from '@radix-ui/react-icons';
14
15type SelectFieldProps = {
16  // Instead of using the `FieldMetadata` type, we will use the `FieldName` type
17  // We can also restrict the type of the field it accepts through its generics
18  name: FieldName<string>;
19  options: Array<string>;
20};
21
22function Select({ name, options }: SelectFieldProps) {
23  const [meta] = useField(name);
24  const control = useInputControl(meta);
25
26  return (
27    <Select.Root
28      name={meta.name}
29      value={control.value}
30      onValueChange={(value) => {
31        control.change(value);
32      }}
33      onOpenChange={(open) => {
34        if (!open) {
35          control.blur();
36        }
37      }}
38    >
39      {/* ... */}
40    </Select.Root>
41  );
42}
43
44function Example() {
45  const [form, fields] = useForm();
46
47  return (
48    <FormProvider context={form.context}>
49      <form id={form.id}>
50        <div>
51          <label>Color</label>
52          <Select name={fields.color.name} options={['red', 'green', 'blue']} />
53          <div>{fields.color.errors}</div>
54        </div>
55        <button>Submit</button>
56      </form>
57    </FormProvider>
58  );
59}

#Examples

Here you can find examples integrating with some of the popular UI libraries.

We are looking for contributors to help preparing examples for more UI libraries, like Radix UI and React Aria Component.