Integrations

BlueForm is fully headless — it ships no styles and has no opinion on how your fields look. This makes it compatible with any React UI library without adapters, plugins, or special configuration.

Sample form

Every recipe in this section builds the same form: four field types (text, longText, select, checkbox), a nested settings section, and an addresses array field.

// This config is identical across every integration in this section
config={{
  name:     { type: 'text',     label: 'Name',     rules: { required: 'Required' } },
  password: { type: 'text',     label: 'Password', props: { type: 'password' } },
  bio:      { type: 'longText', label: 'Bio' },
  role:     { type: 'select',   label: 'Role',     props: { options: [...] } },
  settings: {
    type: 'section',
    label: 'Settings',
    render: ({ label, children }) => <SomeCard title={label}>{children}</SomeCard>,
    props: {
      nested: true,
      config: defineConfig<UserProfile['settings']>({
        newsletter: { type: 'checkbox', label: 'Newsletter' },
        theme:      { type: 'select',   label: 'Theme', props: { options: [...] } },
      }),
    },
  },
  addresses: {
    type: 'array',
    label: 'Address book',
    render: () => <AddressArrayField />,
    props: {
      config: defineConfig<UserProfile['addresses'][number]>({
        street: { type: 'text', label: 'Street' },
        city:   { type: 'text', label: 'City' },
      }),
    },
  },
}}

This config describes what the form collects. It has zero knowledge of the UI library — swapping libraries almost never touches it.

What changes, what doesn't

Migrating from one UI library to another basically means rewriting field components and nothing else.

ChangesStays the same
Field componentsUI library imports, prop names, event signaturesuseField call, destructuring
Section / array shellLayout component (Card, Fieldset, View...)render prop pattern, useArrayField
AppButton styles, visual groupingForm config, defineConfig, submit logic
form.setup.tsNothingEverything

Adapting event signatures

Different libraries expose different onChange signatures — some return the value directly, others wrap it in an event object, others use a custom object. BlueForm doesn't impose any convention. onChange comes from RHF's useController, which accepts both a native event and a plain value:

// Both work — RHF handles either
onChange(e);             // RHF unwraps e.target.value internally
onChange(e.target.value) // explicit clean value passed directly

For better maintainability, always pass a clean value explicitly:

// ✅ Recommended — explicit, readable, no reliance on RHF internals

// Value returned directly — Arco, TDesign, Semi, Mantine Select...
<Input onChange={(val) => onChange?.(val)} />

// Value wrapped in a native event — shadcn, Chakra Input, React Native Web...
<Input onChange={(e) => onChange?.(e.target.value)} />

// Value in a custom object — Chakra Checkbox...
<Checkbox onCheckedChange={({ checked }) => onChange?.(checked)} />

// ⚠️ Also works, but implicit — depends on RHF unwrapping the event
<Input onChange={onChange} />

Bring your own library

Don't see your library here? The pattern is always the same:

  1. Call useField inside your field component and destructure what you need
  2. Map value → the library's value prop, call onChange?.(value) inside the library's change handler
  3. Use errorMessage, label, disabled, visible to wire up the rest

If you can render an input, you can integrate with BlueForm.

Validation libraries

BlueForm delegates validation entirely to React Hook Form's resolver system, which means any library supported by @hookform/resolvers works without any changes to your field components or form config. Define your schema using your preferred library, then pass its resolver via formOptions:

// 1. define schema (and infer type)
const schema = z.object({
  name: z.string().min(1, 'Required'),
  age:  z.number().min(18),
})
type FormData = z.infer<typeof schema>

// 2. pass resolver via formOptions
<Form<FormData>
  formOptions={{ resolver: zodResolver(schema) }}
/>

When a resolver is provided, validation is fully handled by the schema — no rules needed on any field. Your config becomes purely structural: field type, label, and props only.

Schema-level defaults (e.g. .default() in Zod) are not picked up by React Hook Form at initialization — the resolver only runs at validation time, not at mount. Always set initial values via defaultValues or the defaultValue prop on individual fields.