React Hook Form

Merid's form controls are thin wrappers over native elements, so most of them work with React Hook Form's register unchanged. The three controls that hold their own state — Select, RadioGroup and Switch — go through Controller.

Can edit projects and invite people.Read-only. Free on every plan.

Which API for which control

ControlUseWhy
Input, Textarea, NativeSelectregisterThey forward ref, name, onChange and onBlur to the native element.
CheckboxregisterRenders a real <input type="checkbox">; the value is a boolean.
SelectControllerA custom listbox. It takes value / onValueChange, not a change event.
RadioGroup + RadioControllerThe group owns the selected value and roving focus.
SwitchControllerA <button role="switch">; takes checked / onCheckedChange.
SegmentedControlControllerSame as Select: value / onValueChange.

Errors

Pass the message to Field's error prop. Field then sets aria-invalid="true" on the control, adds the message to its aria-describedby, and shows the invalid style. When error is undefined the wiring is removed again, so no extra state is needed.

tsx
<Field label="Email" required error={errors.email?.message}>
  <Input type="email" {...register("email", { required: "Enter an email address." })} />
</Field>

Checkbox has no Field wrapper of its own label, so pass invalid and render the message yourself (as in the example above).

Select inside Field

Select.Trigger does not read Field's context yet, so give both the same id: the Field label then points at the trigger, and pass invalid from fieldState. Call field.onBlur when the listbox closes so mode: "onTouched" validates at the right time.

tsx
<Field id="plan" label="Plan" error={fieldState.error?.message}>
  <Select.Root value={field.value} onValueChange={field.onChange}
    onOpenChange={(open) => { if (!open) field.onBlur(); }}>
    <Select.Trigger id="plan" ref={field.ref} invalid={fieldState.invalid} />
    <Select.Content>…</Select.Content>
  </Select.Root>
</Field>

Passing ref={field.ref} lets React Hook Form focus the trigger when validation fails on submit.

Submit state

Use formState.isSubmitting for the button's loading prop. A loading Button keeps its width, shows a spinner, sets aria-busy and ignores clicks, so a second submit cannot be fired while the first is in flight.

Schema validation

To validate with a schema instead of rules, see Validation with Zod.