Integrating with UI Libraries
In this guide, we'll walk through how to integrate Conform with custom input components.
#Concept
Conform supports native inputs out of the box by listening for form events like input, focusout, and reset on the document. This means simple custom components that wrap native elements (e.g. a styled <input>) typically work without extra setup.
However, more complex inputs like <Select />, <DatePicker />, or custom file uploaders often involve additional layers of abstraction. These layers interfere with how the browser emits native form events, making it harder for Conform to track user interactions.
To solve this, we provide the useControl hook. It lets you manually connect a hidden native form control to a custom UI component and dispatch native events in response to user interaction.
#Emulating Native Inputs
There are multiple ways to connect a native form control with useControl:
In some cases, you may be able to re-use an input rendered by the UI library. This can help with progressive enhancement — for instance, when a user clicks on a styled label that toggles a real hidden checkbox using standard HTML behavior. The form still submits correctly even if JavaScript hasn't loaded.
But not all custom inputs enable this. Some UI libraries render hidden inputs and update their value via JavaScript after interacting with entirely separate elements. These setups won't work before JavaScript loads and may trigger their event handler in an unexpected order — for example, calling
onChangebefore updating the input's value.Because
useControlitself requires JavaScript, the benefits of re-using library-provided inputs are marginal. We recommend rendering your own native form control unless you're confident it helps with progressive enhancement and behaves as expected.
When you render your own native form control, it should be the only named control representing that field. If the UI library also renders a form control, omit its name or disable its form integration to avoid duplicate entries in FormData.
The useControl hook gives you an object that manages the registered form control and provides methods to trigger form events. Here are the main steps to integrate it:
Register a native form control
- Render a hidden native control element (for example, with
BaseControlor a native<input hidden />) and register it withcontrol.register(). - This serves as the authoritative source of the value and emits native form events.
- Choose the native form control according to the form data you want to produce, not the type of custom component you are integrating.
useControllets the custom component read and update that control.
- Render a hidden native control element (for example, with
Emit form events
- Use
control.change()when the component reports a new value. - Call
control.blur()when focus or interaction leaves the logical control. A directonBlurworks for simple controls, but composite or portalled controls may need to ignore focus movement between descendants or use a library-specific close callback.
- Use
Make it controlled
- Use
control.value,control.options,control.checked, orcontrol.filesto sync simple custom components with the current state. - Use
control.payloadwhen the control can hold structured data.
- Use
Delegate focus (optional)
- If the registered form control is hidden, use the
onFocuscallback inuseControlto forward focus to the custom input for accessibility.
- If the registered form control is hidden, use the
Example
1import { useControl } from '@conform-to/react/future';
2import { useForm } from '@conform-to/react';
3import { Input } from 'custom-ui-library';
4
5function MyInput({ name, defaultValue }) {
6 const control = useControl({ defaultValue });
7
8 return (
9 <>
10 <input name={name} ref={control.register} hidden />
11 <Input
12 value={control.value}
13 onChange={(value) => control.change(value)}
14 onBlur={() => control.blur()}
15 />
16 </>
17 );
18}Input Type Differences
The native form control registered with useControl determines how the field contributes to FormData. The custom component does not need to resemble that control. Here's a quick reference:
Text Inputs: Straightforward. When empty, the value is an empty string.
Checkboxes / Radios:
- Default value is
'on'unlessvalueis specified. - If checked, the value is submitted; if unchecked, it's omitted.
- Default value is
Select (single): Value is the selected option's
value. Defaults to the first option.Select (multiple): Represents an array of selected values. If none selected, no entry in
FormData.File Inputs: Yields one or more
Fileobjects. When empty, it still contributes an emptyFileobject inFormData.Other inputs: Behave like text inputs. Their
typeadds context but doesn't affect how data is submitted.
Reusing Component Mappings
If the same field metadata is repeatedly mapped to your custom component props, configureForms can expose that mapping as typed custom metadata. Keep library-specific value conversion, event handling, focus delegation, and ARIA wiring inside the custom component; use custom metadata only to remove repetitive prop mapping at call sites.
#Examples
We've prepared examples for integrating with popular UI libraries:
If the library you're using isn't listed or you run into issues, open a discussion — we're happy to help. Contributions are also welcome if you'd like to share an example.