react-formbridge
Browse documentation
Field buildersv1.0.2

field.custom()

Escape hatch for UI that deserves a custom renderer while keeping the rest of the form runtime.

  • Use it when the built-in field types are not enough
  • You still keep schema typing, validation, state, submit lifecycle, and the same generated field map
  • If the value model is already one of the built-in field types and you only want to replace the UI, prefer form.fieldController(name) before reaching for field.custom()
Custom.tsxtsx
1const schema = {
2 rating: field.custom(0)
3 .label('Rating')
4 .render(({ label, value, onChange, error }) => (
5 <div>
6 <p>{label}</p>
7 {[1,2,3,4,5].map((n) => (
8 <button key={n} type="button" onClick={() => onChange(n)}>
9 {value >= n ? '★' : '☆'}
10 </button>
11 ))}
12 {error ? <p>{error}</p> : null}
13 </div>
14 ))
15 .validate((value) => (value > 0 ? null : 'Pick a rating')),
16}
17
18const { Form, fields } = useFormBridge(schema)
19
20<Form onSubmit={save}>
21 <fields.rating />
22 <Form.Submit>Send</Form.Submit>
23</Form>

Defaults, inheritance & field methods

field.custom(defaultValue) returns a typed BaseFieldBuilder - no extra methods, just the shared base surface (see Builder basics).

KeyDescription
defaultValueRequired - stays typed through the generated field
labelOptional at construction, usually added with label('...')
render(fn)Main escape hatch - keeps the form runtime while replacing the UI
fieldControllerIf you need custom UI for a built-in field type (select, masked, phone), prefer form.fieldController(name) instead

render(fn) receives: name, label, value, placeholder, error, touched, dirty, validating, disabled, hint, options, otpLength, onChange, onBlur, onFocus, allValues

Custom-field methods:

MethodTypeDescription
--field.custom(defaultValue) does not add methods on top of the base builder; it is the raw BaseFieldBuilder escape hatch.

Recipes

Patterns that showcase custom-specific strengths.

Star rating widget

StarRating.tsxtsx
1const schema = {
2 rating: field.custom(0)
3 .label('Rating')
4 .render(({ value, onChange }) => (
5 <Stars value={value} onChange={onChange} />
6 ))
7 .validate((value) => (value > 0 ? null : 'Pick a rating')),
8}

Trip window picker

TripWindow.tsxtsx
1const schema = {
2 travelWindow: field
3 .custom<{ start: Date | null; end: Date | null }>({
4 start: null,
5 end: null,
6 })
7 .label('Date range')
8 .render((props) => <DateRangePicker {...props} />),
9}

Color picker

ColorPicker.tsxtsx
1const schema = {
2 brandColor: field.custom('#000000')
3 .label('Brand color')
4 .render(({ value, onChange }) => (
5 <ColorWheel value={value} onChange={onChange} />
6 )),
7}

When to use fieldController instead

If the value model is already a built-in type (select, masked, phone, …) and you only need a different UI, keep the schema field as-is and drive the UI through form.fieldController(name):

FieldControllerAlternative.tsxtsx
1const schema = {
2 plan: field.select('Plan').options(PLAN_OPTIONS).required(),
3}
4
5const form = useFormBridge(schema)
6const plan = form.fieldController('plan')
7
8<MyCustomSegmentedControl
9 value={plan.value}
10 options={plan.options}
11 onChange={plan.setValue}
12/>