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} />;
}