Custom field types
Register your own field components alongside the 44 built-ins using the field registry.
The field registry
Every field type maps to a React component via the registry. The default registry contains all 44 built-in field components. You can add your own types or override existing ones.
import { defaultRegistry } from '@squaredr/fieldcraft-react'
const registry = {
...defaultRegistry,
color_picker: ColorPickerField,
pain_scale: PainScaleField,
}Pass it to FormRenderer:
<FormRenderer schema={schema} components={registry} onSubmit={handleSubmit} />FieldProps
Every field component receives the same props interface:
type FieldProps = {
field: Question // The field definition from the schema
value: unknown // Current value
error?: string[] // Validation errors (if any)
touched: boolean // Whether the user has interacted with this field
disabled: boolean // Whether the field is disabled
onChange: (value: unknown) => void // Call this when the value changes
onBlur: () => void // Call this when the field loses focus
theme: FormEngineTheme // Current theme
customProps?: Record<string, unknown> // Custom props from schema
}Building a custom field
Here's a complete example — a pain scale field for a medical intake form:
import type { FieldProps } from '@squaredr/fieldcraft-react'
export function PainScaleField({ field, value, error, touched, disabled, onChange, onBlur }: FieldProps) {
const current = typeof value === 'number' ? value : null
return (
<div>
<label>{field.label}</label>
{field.helpText && <p className="fc-help-text">{field.helpText}</p>}
<div style={{ display: 'flex', gap: 8 }}>
{Array.from({ length: 11 }, (_, i) => (
<button
key={i}
type="button"
disabled={disabled}
onClick={() => { onChange(i); onBlur() }}
style={{
width: 36,
height: 36,
borderRadius: '50%',
border: current === i ? '2px solid #0066cc' : '1px solid #ddd',
background: current === i ? '#0066cc' : getScaleColor(i),
color: current === i ? '#fff' : '#333',
cursor: disabled ? 'not-allowed' : 'pointer',
}}
>
{i}
</button>
))}
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 12, color: '#999' }}>
<span>No pain</span>
<span>Worst pain</span>
</div>
{touched && error?.map((e) => (
<p key={e} style={{ color: 'red', fontSize: 13 }}>{e}</p>
))}
</div>
)
}
function getScaleColor(n: number): string {
if (n <= 3) return '#dcfce7'
if (n <= 6) return '#fef9c3'
return '#fee2e2'
}Using it in a schema
Reference your custom type by its registry key:
{
id: 'pain_level',
type: 'pain_scale', // Matches the registry key
label: 'Rate your current pain level',
required: true,
helpText: '0 = no pain, 10 = worst imaginable pain',
}The QuestionType union includes (string & {}), so any string is a valid type — no need to modify core types.
Using customProps
For type-specific configuration that doesn't fit the built-in config system, use customProps:
{
id: 'brand_color',
type: 'color_picker',
label: 'Choose your brand colour',
customProps: {
swatches: ['#0066cc', '#8b5cf6', '#dc2626', '#22c55e'],
allowCustom: true,
},
}Access them in your component:
function ColorPickerField({ field, value, onChange, onBlur }: FieldProps) {
const swatches = (field.customProps?.swatches as string[]) || []
const allowCustom = field.customProps?.allowCustom as boolean
return (
<div>
{swatches.map((color) => (
<button
key={color}
onClick={() => { onChange(color); onBlur() }}
style={{ background: color, width: 32, height: 32 }}
/>
))}
{allowCustom && (
<input
type="color"
value={(value as string) || '#000000'}
onChange={(e) => onChange(e.target.value)}
onBlur={onBlur}
/>
)}
</div>
)
}Overriding built-in fields
You can replace any built-in field type:
const registry = {
...defaultRegistry,
rating: MyCustomRatingField, // Replaces the built-in RatingField
}Your replacement receives the same FieldProps — it's a drop-in swap.
Registry utilities
import { createFieldRegistry, mergeRegistries } from '@squaredr/fieldcraft-react'
// Create a new registry
const medical = createFieldRegistry({ pain_scale: PainScaleField })
// Merge registries — later ones override
const combined = mergeRegistries(defaultRegistry, medical)Next steps
- Theming — style your custom fields with theme tokens
- FormRenderer — pass the registry to the renderer
- Field types — the 44 built-in types for reference