Custom Renderers
Two patterns for building custom field rendering. Choose based on whether you need a reusable renderer component (registered in fieldRendererMap) or inline composition per field.
When to use which
| Situation | Pattern |
|---|---|
| Composing specific fields inline | FormField compound parts |
| Building a reusable renderer for all fields of one type | FieldComposer + fieldRendererMap |
For most BYOC cases, FormField compound is simpler. See Customization.
FieldComposer — reusable renderer components
Use FieldComposer when building a renderer component to register in fieldRendererMap. Formwright handles the shell (label, description, error, help) — you provide the control. Works with fieldSlots.
If you only need to change how an input looks globally, use slots or fieldRendererMap instead.
Step 1: Implement the renderer
import { FieldComposer, type RenderFieldProps } from "formwright/react";
export function CountrySelectRenderer(props: RenderFieldProps) {
return (
<FieldComposer
field={props.field}
state={props.state}
label={props.field.uiField?.label ?? props.field.path}
description={props.field.uiField?.description}
helpText={props.field.uiField?.helpText}
error={props.error}
>
<select
value={(props.value as string | undefined) ?? ""}
onChange={(e) => props.onChange(e.target.value)}
onBlur={props.onBlur}
disabled={props.state.disabled}
aria-invalid={Boolean(props.error)}
>
{(props.options ?? []).map((opt) => (
<option key={String(opt.value)} value={String(opt.value)}>
{opt.label}
</option>
))}
</select>
</FieldComposer>
);
}
RenderFieldProps reference
| Prop | Type | Description |
|---|---|---|
field | ResolvedFieldModel | Schema definition + UI metadata |
state | DerivedFieldState | visible, disabled, required |
value | unknown | Current RHF field value |
onChange | (value: unknown) => void | Update field value |
onBlur | () => void | Trigger field blur |
error | string | undefined | Validation error message |
options | SelectOption[] | Options from datasource (if any) |
loading | boolean | Datasource is loading |
path | string | Field path (dot notation) |
Step 2: Register in the renderer map
import { FormRuntimeRoot } from "formwright/react";
import { CountrySelectRenderer } from "./CountrySelectRenderer";
<FormRuntimeRoot
rootLayoutId="root"
fieldRendererMap={{
"country-select": CountrySelectRenderer,
}}
/>
Step 3: Set the renderer key on the field
const countryField = field.select("country", {
label: "Country",
renderer: "country-select", // matches the key in fieldRendererMap
dataSource: "countries",
});
Using the default renderer maps as a base
If you want to replace only one or two renderers while keeping the defaults for everything else:
import { createDefaultRendererMaps } from "formwright/react";
const { fieldRendererMap } = createDefaultRendererMaps();
<FormRuntimeRoot
rootLayoutId="root"
fieldRendererMap={{
...fieldRendererMap,
"country-select": CountrySelectRenderer,
"date": MyDatePickerRenderer,
}}
/>
Full replacement (no FieldComposer)
For complete control over rendering — label, error, wrapper and all:
import type { FieldRendererComponent } from "formwright/react";
const SignaturePad: FieldRendererComponent = ({ value, onChange, error, state }) => {
return (
<div className="signature-field">
<canvas
data-disabled={state.disabled}
onMouseUp={() => onChange(captureSignature())}
/>
{error && <span className="error">{error}</span>}
</div>
);
};
Then register it in fieldRendererMap the same way.