Generated fields are the default path, but the runtime still leaves room for design-system wrappers and fully bespoke inputs.
- Use
fieldController(name)when the value model is still one of the built-in field types and you only want custom UI - Use
field.custom(defaultValue)when the field needs a new value model - Keep styling in
globalDefaults, local field overrides, or host components so the schema stays focused on behavior
Use field.custom() for new value models
If the built-in field families are close but not quite right, keep the new value model typed in the schema and replace the renderer completely.
CustomRating.tsxtsx
| 1 | const schema = { |
| 2 | rating: field |
| 3 | .custom(0) |
| 4 | .label('Rating') |
| 5 | .render(({ label, value, onChange, error }) => ( |
| 6 | <div> |
| 7 | <p>{label}</p> |
| 8 | {[1, 2, 3, 4, 5].map((step) => ( |
| 9 | <button key={step} type="button" onClick={() => onChange(step)}> |
| 10 | {value >= step ? '★' : '☆'} |
| 11 | </button> |
| 12 | ))} |
| 13 | {error ? <p>{error}</p> : null} |
| 14 | </div> |
| 15 | )) |
| 16 | .validate((value) => (value > 0 ? null : 'Pick a rating')), |
| 17 | } |
Style the same runtime in different ways
The product shell can evolve without rewriting the field semantics. Keep styling in the UI layer and keep behavior in the schema.