Mental Model
Formwright separates form concerns into three independent layers. Understanding this separation makes it easier to decide where to put custom logic.
The three layers
Schema → Runtime → Renderer
Schema (formwright/schema) — a plain data structure that describes your form: which fields exist, how they're laid out, what rules govern their behavior. No React, no side effects.
Runtime (formwright/core) — evaluates the schema against current form values and produces derived state: which fields are visible, disabled, required; what their values should be. Also pure and synchronous.
Renderer (formwright/react) — subscribes to the runtime's derived state via React context and renders UI. This is the only layer with React dependencies.
What this means in practice
| I want to... | I change... |
|---|---|
| Add a new field or layout section | Schema (buildForm) |
| Add a show/hide rule | Schema (rules) |
| Change how a field type looks | Renderer (custom renderer or slot) |
| Replace one input with a design-system component | Renderer (fieldRendererMap) |
| Add custom conditional logic | Plugin (operator) |
| Add a new field effect | Plugin (effect) |
| Load options from an API | Plugin (datasource) or registerAsyncPlugins() |
How rendering works
FormRuntimeProviderwatches RHF field values viawatch()- On each change, calls
runtime.evaluate(values)— synchronous, pure - Produces a snapshot:
{ fieldState, layoutState, fieldOptions, values } - Passes snapshot to all children via React context
FormRuntimeRootwalks the layout tree and renders each node
Async work (remote data sources) happens in the useDatasourceOptions hook — not in evaluate().
Import paths
Formwright is published as a single formwright package with sub-path exports:
import { buildForm, field, layout } from "formwright/schema";
import { createFormRuntime } from "formwright/core";
import { FormRuntimeProvider, FormRuntimeRoot } from "formwright/react";
import { registerBasicPlugins, registerAsyncPlugins } from "formwright/plugins";
You can also import from the root:
import { buildForm, createFormRuntime, FormRuntimeProvider } from "formwright";
Customization decision guide
| Situation | Approach |
|---|---|
| Change label style, error style, or wrapper | fieldSlots |
| Replace one input type with a design system component | fieldRendererMap |
| Build a complex domain widget (date picker, signature pad) | fieldRendererMap + FieldComposer |
| Replace a layout node's rendering | layoutRendererMap |
| Add new conditional operators (fuzzy match, date comparison) | Custom operator plugin |
| Add new field effects | Custom effect plugin |
| Add a new field type | Custom field plugin |
See Customization for examples of each.