Skip to main content

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

SituationPattern
Composing specific fields inlineFormField compound parts
Building a reusable renderer for all fields of one typeFieldComposer + 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

CountrySelectRenderer.tsx
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

PropTypeDescription
fieldResolvedFieldModelSchema definition + UI metadata
stateDerivedFieldStatevisible, disabled, required
valueunknownCurrent RHF field value
onChange(value: unknown) => voidUpdate field value
onBlur() => voidTrigger field blur
errorstring | undefinedValidation error message
optionsSelectOption[]Options from datasource (if any)
loadingbooleanDatasource is loading
pathstringField path (dot notation)

Step 2: Register in the renderer map

Form.tsx
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.