Docs
v1.3.14
GitHubSite
React / Custom field types

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
On this page