@letar/forms

Form Component

The main Form compound component and its sub-components

Form

The root compound component. Provides form context and validation.

<Form
  schema={Schema}
  initialValue={data}
  onSubmit={async ({ value }) => { ... }}
>
  {/* Field components */}
</Form>

Props

PropTypeDescription
schemaZodObjectZod validation schema
initialValueTInitial form data
onSubmit(props: { value: T }) => Promise<void>Submit handler
onError(errors: FieldErrors) => voidError handler
offlineOfflineConfigOffline mode configuration
persistencePersistenceConfigAuto-save to localStorage
debugboolean | 'force'Show DebugValues (FromSchema only)
onFieldChangeRecord<string, (value, api) => void>React to field value changes
middlewareFormMiddlewarebeforeSubmit, afterSuccess, onError
childrenReactNodeForm content

onFieldChange

Auto-generate slug, sync dependent fields, or recalculate totals:

<Form
  schema={Schema}
  initialValue={data}
  onSubmit={save}
  onFieldChange={{
    name: (value, { setFieldValue }) => {
      setFieldValue('slug', transliterate(String(value)))
    },
    quantity: (value, { setFieldValue, getFieldValue }) => {
      const price = getFieldValue('price') as number
      setFieldValue('total', (value as number) * price)
    },
  }}
>

See Field Watchers for patterns.

persistence

<Form
  schema={Schema}
  initialValue={data}
  onSubmit={handleSubmit}
  persistence={{
    key: 'checkout-form',
    debounceMs: 500,
    dialogTitle: 'Restore draft?',
    dialogDescription: 'You have unsaved changes.',
    restoreLabel: 'Restore',
    discardLabel: 'Discard',
  }}
>

Form.Field.*

40+ field components. See Field Types for full list.

Common props shared by all fields:

PropTypeDescription
namestringField name matching schema key
labelstringOverride label from schema
placeholderstringOverride placeholder from schema
helperTextstringAdditional guidance text
requiredbooleanMark as required
disabledbooleanDisable the field

Form.Group

Group fields into a nested object:

<Form.Group name="address">
  <Form.Field.String name="street" />
</Form.Group>

Form.Group.List

Dynamic array of items:

<Form.Group.List name="items" sortable>
  <Form.Field.String name="title" />
  <Form.Group.List.Button.Add />
  <Form.Group.List.Button.Remove />
</Form.Group.List>

Form.Steps

Multi-step wizard. See Multi-Step Forms.

Form.Watch

Renderless component that fires a callback when a field value changes. Group-aware — resolves paths relative to the nearest Form.Group.

<Form.Watch
  field="country"
  onChange={(value, { setFieldValue }) => {
    setFieldValue('currency', currencyMap[String(value)])
  }}
/>
PropTypeDescription
fieldstringField name to watch (relative to group)
onChange(value: unknown, api: FieldChangeApi) => voidCallback on change

See Field Watchers for patterns and comparison with onFieldChange.

Form.When

Conditional rendering. See Conditional Fields.

Form.InfoBlock

Contextual info/warning/error/success/tip message block inside the form. Based on Chakra UI Alert.

<Form.InfoBlock variant="warning" title="Note">
  Company accounts require additional verification.
</Form.InfoBlock>
PropTypeDefaultDescription
variant'info' | 'warning' | 'error' | 'success' | 'tip''info'Visual style
titleReactNodeBlock heading
childrenReactNodeContent
appearance'subtle' | 'surface' | 'outline' | 'solid''subtle'Chakra Alert variant
size'sm' | 'md' | 'lg''md'Size

Form.Divider

Horizontal separator for visual grouping. Based on Chakra UI Separator.

<Form.Divider label="Contact Information" />
<Form.Divider variant="dashed" />
<Form.Divider label="Phones" icon={<LuPhone />} />
PropTypeDefaultDescription
labelReactNodeText label
iconReactNodeIcon before label
variant'solid' | 'dashed' | 'dotted''solid'Line style
size'xs' | 'sm' | 'md' | 'lg''xs'Thickness
colorPalettestring'gray'Color

Form.Field.Hidden

Hidden field — not rendered in DOM, only participates in form state.

<Form.Field.Hidden name="utm_source" value="landing" />
PropTypeDescription
namestringField name
valueunknownValue to sync into form state

See Utility Components for examples.

Form.Field.Calculated

Auto-computed read-only field that recalculates when dependent fields change.

<Form.Field.Calculated
  name="total"
  label="Total"
  compute={(v) => (Number(v.price) || 0) * (Number(v.qty) || 0)}
  format={(v) => `${Number(v).toLocaleString()} ₽`}
  deps={['price', 'qty']}
/>
PropTypeDefaultDescription
namestringField path in form state
compute(values) => unknownCompute function (required)
labelstringField label
format(value) => stringDisplay formatter
depsstring[]Dependency fields for optimized recalculation
debouncenumber0Debounce delay in ms
hiddenbooleanfalseCompute without rendering

See Calculated Fields for full guide.

Form.Errors

Error summary component:

<Form.Errors title="Please fix:" showFieldNames />

Form.Button.Submit

Submit button that automatically shows a loading spinner while the form is submitting. It subscribes to the form's isSubmitting state — when onSubmit returns a Promise, the button stays in loading state until it resolves.

Props

PropTypeDefaultDescription
childrenReactNode"Submit"Button content — text, icon, or any element
loadingTextReactNodeText shown during submit (replaces children)
disabledbooleanfalseDisable the button
colorPalettestringColor palette ("brand", "blue", "red")
size"xs" | "sm" | "md" | "lg""md"Button size
variant"solid" | "outline" | "ghost" | "subtle""solid"Button variant
widthstring | numberButton width ("100%" for full width)

Basic usage

<Form.Button.Submit>Save</Form.Button.Submit>

Loading text

Show custom text while submitting:

<Form.Button.Submit loadingText="Saving...">Save</Form.Button.Submit>

Custom styling

<Form.Button.Submit colorPalette="brand" size="lg" width="100%">
  Create Account
</Form.Button.Submit>

How loading works

The button automatically enters loading state when onSubmit is an async function. To see the effect, add a delay to your handler:

<Form
  schema={Schema}
  initialValue={data}
  onSubmit={async ({ value }) => {
    await new Promise((r) => setTimeout(r, 1500))
    alert(`Submitted: ${JSON.stringify(value)}`)
  }}
>
  <Form.Field.String name="name" />
  <Form.Button.Submit loadingText="Sending...">Send</Form.Button.Submit>
</Form>

Form.Button.Reset

Reset form to initial values:

<Form.Button.Reset>Reset</Form.Button.Reset>

Form.FromSchema

Auto-generate entire form from schema:

<Form.FromSchema schema={Schema} initialValue={data} onSubmit={handleSubmit} submitLabel="Create" columns={2} />

Form.Builder

Generate form from JSON configuration:

<Form.Builder
  schema={Schema}
  initialValue={data}
  onSubmit={handleSubmit}
  fields={[
    { name: 'title', type: 'string' },
    { name: 'price', type: 'currency', section: 'Pricing' },
  ]}
/>

Form.AutoFields

Auto-generate fields from schema with filtering:

// All fields from schema
<Form.AutoFields />

// Only specific fields
<Form.AutoFields include={['name', 'email', 'role']} />

// Exclude certain fields
<Form.AutoFields exclude={['id', 'createdAt']} />

// Custom wrapper
<Form.AutoFields fieldWrapper={({ name, children }) => (
  <Box key={name} mb={4}>{children}</Box>
)} />
PropTypeDescription
includestring[]Only render these fields
excludestring[]Skip these fields
recursivebooleanAuto-generate nested objects (default: true)
fieldWrapperComponentCustom wrapper for each field

Form.DebugValues

Interactive JSON inspector showing real-time form values:

<Form.DebugValues />
<Form.DebugValues title="Current State" collapsed={3} />
PropTypeDescription
titlestringBlock title (default: "Form Values")
collapsednumberTree expansion depth (default: 2)
showInProductionbooleanShow in production (default: false)

Form.DirtyGuard

Warns user about unsaved changes when navigating away:

<Form.DirtyGuard title="Unsaved changes" message="You have unsaved changes. Are you sure you want to leave?" />

Form.Subscribe

Reactive subscription to form values — for live previews:

<Form.Subscribe>{(values) => <Preview data={values} />}</Form.Subscribe>

See Controlled State for patterns.

createForm

Create extended form with custom fields and selects. See createForm guide.

On this page