FieldCraftDocsServicesBlogWork With Me →

Custom Fields

Build your own field components for FieldCraft.

A custom field is a React component that implements the FieldProps interface. Register it in the components prop and reference it by type in your schema.

FieldProps Interface

type FieldProps = {
field: Question; // Schema question metadata
value: unknown; // Current field value
error?: string[]; // Validation errors (or undefined)
touched: boolean; // Has the field been interacted with
disabled: boolean; // Is the field disabled
onChange: (value: unknown) => void; // Call to update the value
onBlur: () => void; // Call on blur to mark as touched
theme: FormEngineTheme; // Current theme object
};

Minimal Example

import type { FieldProps } from "@squaredr/fieldcraft-react";
import { FieldWrapper, fieldAria } from "@squaredr/fieldcraft-react";

function ColorPickerField({ field, value, error, touched, onChange, onBlur }: FieldProps) {
const hasError = touched && !!error?.length;

return (
<FieldWrapper field={field} error={error} touched={touched}>
<input
{...fieldAria(field, hasError)}
type="color"
value={typeof value === "string" ? value : "#000000"}
onChange={(e) => onChange(e.target.value)}
onBlur={onBlur}
/>
</FieldWrapper>
);
}

FieldWrapper

FieldWrapper is a layout component that renders the label, help text, and error messages around your input. It handles:

  • The field label with a required indicator (*) when applicable
  • Help text from field.helpText
  • Error messages — only shown when touched is true
type FieldWrapperProps = {
field: Question; // From FieldProps
error?: string[]; // From FieldProps
touched: boolean; // From FieldProps
children: React.ReactNode;
className?: string;
hideLabel?: boolean; // Omit the label (e.g., for hidden fields)
};

fieldAria Helper

fieldAria returns accessibility attributes for your input element:

import { fieldAria } from "@squaredr/fieldcraft-react";

const aria = fieldAria(field, hasError);
// Returns:
// {
// id: "field_id",
// "aria-describedby": "field_id-help field_id-error",
// "aria-invalid": true,
// "aria-required": true,
// }

The aria-describedby links to the help text and error elements rendered by FieldWrapper. Spread it onto your input.

Full Example: Star Rating

import type { FieldProps } from "@squaredr/fieldcraft-react";
import { FieldWrapper, fieldAria } from "@squaredr/fieldcraft-react";

function StarRatingField({ field, value, error, touched, disabled, onChange, onBlur }: FieldProps) {
const maxStars = field.config?.maxStars ?? 5;
const current = typeof value === "number" ? value : 0;
const hasError = touched && !!error?.length;

return (
<FieldWrapper field={field} error={error} touched={touched}>
<div
{...fieldAria(field, hasError)}
role="radiogroup"
aria-label={field.label}
className="flex gap-1"
onBlur={onBlur}
>
{Array.from({ length: maxStars }, (_, i) => (
<button
key={i}
type="button"
disabled={disabled}
onClick={() => onChange(i + 1)}
aria-label={`${i + 1} star${i > 0 ? "s" : ""}`}
aria-checked={current === i + 1}
className={current >= i + 1 ? "text-yellow-400" : "text-gray-300"}
>

</button>
))}
</div>
</FieldWrapper>
);
}

Registering Your Field

import { FormEngineRenderer } from "@squaredr/fieldcraft-react";

<FormEngineRenderer
schema={schema}
components={{
color_picker: ColorPickerField,
rating: StarRatingField, // Override built-in rating
}}
onSubmit={handleSubmit}
/>

Using in a Schema

{
id: "brand_color",
type: "color_picker", // Matches registry key
label: "Brand Color",
required: true,
helpText: "Choose your primary brand color",
}

Accessing Type-Specific Config

The field.config object contains type-specific settings from the schema. Use it in your component to customize behavior:

function MySlider({ field, value, onChange, onBlur }: FieldProps) {
const min = field.config?.min ?? 0;
const max = field.config?.max ?? 100;
const step = field.config?.step ?? 1;

return (
<input
type="range"
min={min}
max={max}
step={step}
value={typeof value === "number" ? value : min}
onChange={(e) => onChange(Number(e.target.value))}
onBlur={onBlur}
/>
);
}

Wrapping a Built-In Field

Import a built-in field and wrap it with additional behavior:

import { EmailField } from "@squaredr/fieldcraft-react";
import type { FieldProps } from "@squaredr/fieldcraft-react";

function TrackedEmailField(props: FieldProps) {
const handleChange = (value: unknown) => {
analytics.track("email_entered");
props.onChange(value);
};

return <EmailField {...props} onChange={handleChange} />;
}