01 Software

UI

FormRenderer

Headless form renderer

FormRenderer

A headless component for rendering forms created with the Form Builder plugin. Uses native HTML elements and is fully customizable via render functions.

Basic Usage

import { FormRenderer, toSubmissionData } from '@01.software/sdk/ui/form'

function ContactForm({ form }) {
  const handleSubmit = async (values) => {
    await client.from('form-submissions').create({
      form: form.id,
      submissionData: toSubmissionData(values),
    })
  }

  return <FormRenderer form={form} onSubmit={handleSubmit} />
}

Props

PropTypeDescription
formFormForm Builder form object (required)
onSubmit(data: FormValues) => void | Promise<void>Submit handler (required)
classNamestring<form> CSS class
fieldClassNamestringField wrapper CSS class
buttonClassNamestringSubmit button CSS class
renderField(field, context, defaultRender) => ReactElementCustom field renderer
renderMessage(data: RichTextData) => ReactElementMessage block renderer
renderButton(props: { isSubmitting, label }) => ReactElementCustom button renderer

Supported Field Types

text, email, number, textarea, checkbox, select, country, state, message

Styling

Class Name Props

PropTargetNotes
className<form> elementControls overall form layout
fieldClassNameWrapper <div> around each field (label + input)Applied to the wrapper, not the <input> itself
buttonClassNameSubmit <button>Overrides default button styles

Multi-Column Layout

Fields with a width value (e.g. 50) receive an inline style of width: X%. To create a multi-column layout, apply display: flex and flex-wrap: wrap to the form, and use calc() on each field to account for the gap between columns.

For example, a field with width: 50 renders as:

<div style="width: 50%">
  <label>...</label>
  <input />
</div>

Since the inline width does not account for gaps, you need to override field widths with calc(50% - gap/2) or use CSS to handle spacing.

Tailwind Example

<FormRenderer
  form={form}
  onSubmit={handleSubmit}
  className="flex flex-wrap gap-4"
  fieldClassName="flex flex-col gap-1 [&:has([style*='width'])]:w-[calc(50%-0.5rem)]"
  buttonClassName="mt-4 w-full rounded-lg bg-black px-4 py-2 text-white hover:bg-gray-800"
/>

In this example:

  • className sets the form to a flex container with wrapping and a 1rem gap
  • fieldClassName stacks labels above inputs. The [&:has([style*='width'])] selector targets fields that have an inline width style and overrides them with calc(50% - 0.5rem) to account for the gap
  • buttonClassName styles the submit button with full width and rounded corners

For full control over individual field styling, use renderField to return custom markup per field type.

Custom Field Rendering

<FormRenderer
  form={form}
  onSubmit={handleSubmit}
  renderField={(field, context, defaultRender) => {
    if (field.blockType === 'email') {
      return (
        <div className="custom-email">
          <input
            type="email"
            value={String(context.value)}
            onChange={(e) => context.onChange(e.target.value)}
          />
        </div>
      )
    }
    return defaultRender
  }}
/>

Exports

ExportDescription
FormValuesRecord<string, string | number | boolean>
FormFieldForm field union type
InputFieldInput field type (excludes message)
SubmissionData{ field: string; value: string }[]
FieldRenderContext{ value, onChange }
toSubmissionData(values)Converts FormValuesSubmissionData

On this page