Field types
24 built-in types, registered by registerBuiltInFields(). Every field also takes the shared base properties — label, required, visibleWhen/disabledWhen, and width — covered once in Base props below, not repeated per type.
Base props
Every field type below accepts these 14 on top of its own — documented once here rather than repeated per type.
| Name | Type | Required | Description |
|---|---|---|---|
| name | string | Required | Field key: the RHF register name, and the identifier that conditions, copyFrom/countryFrom/optionsFrom, and dependsOn target by name. Must not contain a dot (validator-enforced — dots are read as nested paths by RHF and the condition engine). |
| label | string | Optional | Visible label text. static and submit ignore it (their own content/text prop is the visible text instead). |
| description | string | Optional | Helper text rendered under the label and wired into aria-describedby alongside any error. |
| badge | string | Optional | Short annotation rendered beside the label — "Required in Germany" — so a conditionally-revealed field can say why it appeared. Part of the field's accessible name, unlike the required mark, which is aria-hidden because the control already exposes required-ness on its own. needs a label to annotate: no label, no badge — which also means static, submit, and hidden ignore it, since they render none. |
| autocomplete | string (HTML autocomplete value) | Optional | The control's autocomplete attribute — "name", "email", "street-address", "postal-code", "address-level2", "bday". WCAG 2.2 SC 1.3.5 (AA) requires one on every field collecting information about the person filling the form; a label alone does not carry the purpose, since "Postleitzahl" and "ZIP" are the same purpose to a person and neither to a parser. Typed as a plain string rather than an enum of the WCAG purposes because the attribute is a grammar around one purpose token — "section-owner-1 name", "shipping street-address", "mobile tel" and "off" are all valid and an allowlist would reject them. reaches the DOM on text, email, password, textarea, number, masked, time, phone and otp — the types whose control is a native text-entry input. date, select and country render a popover behind a button, and HTML ignores the attribute on file, checkbox/switch, radio and segmented; set there it is inert, not an error. phone and otp already default to "tel" and "one-time-code", which an unset value leaves alone. |
| placeholder | string | Optional | Placeholder/prompt text; meaning is per-control (input placeholder, empty-select prompt, unset-date prompt). on group it overrides the "Add" button's label instead of a text placeholder; on file it replaces the dropzone's "Drag files here, or browse" prompt, which is part of the file input's accessible name — so it is read aloud after the label, not just shown; has no effect on radio, segmented, checkbox group, rating, slider, or signature. |
| required | boolean | Optional | Marks the field mandatory: shows a required mark and drives the generated zod schema. on rating it also changes clear behavior (clicking the current value clears it only when optional); radio, segmented, and country can never be cleared once set, regardless of required. No effect on static/submit (no user input) or hidden (always has a value). |
| disabled | boolean | Optional | Statically disables the control; disabledWhen/enabledWhen layer on top of (not instead of) this flag. |
| visibleWhen | ConditionSpec | Optional | Hides the field and excludes it from validation and the submit payload while the spec doesn't match. Value operators only (equals/notEquals/in) — isValid is rejected here, since visibility itself decides which schema gets built. |
| disabledWhen | ConditionSpec | Optional | Disables the field while the spec matches. The only base prop where isValid is allowed (checking a sibling's own schema validity). Mutually exclusive with enabledWhen (validator-enforced). |
| enabledWhen | ConditionSpec | Optional | Inverse of disabledWhen — disabled while the spec does NOT match; reads straight for "enabled once X is valid". Mutually exclusive with disabledWhen. |
| enabledWhenVerified | string | Optional | Keeps the field disabled until the named sibling otp field's code is verified. must reference a sibling of type otp; rejected inside a group (validator-enforced). |
| copyFrom | string | Optional | Mirrors a same-type sibling's value until the user edits this field, then the source wins again on its next change (same semantics as phone's countryFrom). not supported on phone, otp, password, file, signature, group, hidden, static, or submit fields (validator-enforced); a select pair must also match multiple, and a date pair must also match range. |
| width | ResponsiveFieldWidth | Optional | 'full' | 'half' | 'third' | 'quarter', or a per-breakpoint object, on the 12-col grid; unset breakpoints fall back to full rather than cascading from a smaller breakpoint. |
Shared shapes
Shapes referenced by name from more than one field type's props below, explained once here.
- Option
- { label: string; value: string | number; disabled?: boolean }
- One selectable choice. radio/segmented/checkbox compare value with ===; the native <select> compares String(value), so numeric and string values that stringify the same are not distinguished there. disabled greys out just that option.
- Used by:
select,radio,segmented,checkbox,switch - TextRules
- { minLength?; maxLength?; pattern?: string; message?; trim?; allow?: string; matches?: string; matchesMessage? }
- pattern is a string (not a RegExp) so it stays JSON-serializable; trim also normalizes the visible input on blur, not just the parsed payload; allow blocks disallowed characters at typing/paste time (a character-class body, e.g. "A-Za-z "). matches is cross-field equality (confirm password/email): enforced by a form-level refine, never the field's own schema — the isValid oracle behind isValid conditions parses field schemas in isolation and can't see it.
- Used by:
text,email,textarea,password - PasswordComplexity
- { uppercase?; lowercase?; number?; special?: boolean; minLength?: number }
- Each flag adds one line to the live pass/fail checklist rendered under the password input; minLength adds its own line instead of folding into rules.minLength.
- Used by:
password - optionsFrom
- { field: string; map: Record<string, Option[]> }
- field must be a sibling single-value select or country field (validator-enforced — a multi-select or a dynamic optionsFrom source is rejected); map is keyed by String(source value). A source value with no matching key renders an empty, disabled select (dev-only console warning, not a hard error). Not supported inside groups.
- Used by:
select - countryFrom
- string
- Names a sibling country or single-value select field (whose option values are ISO alpha-2 — a country field qualifies by construction, a select's options are validated as ISO codes). The phone field re-syncs its calling code from that source on every change — the source always wins over a manual pick, until the source changes again. Rejected inside groups; a cross-step pairing only dev-warns (source edits made while the phone field is unmounted are skipped until it remounts).
- Used by:
phone - Sibling-bound min/max (date & time)
- minDateField/maxDateField (date), minTimeField/maxTimeField (time): string
- Bounds a field against a sibling date/time field's CURRENT value (e.g. "end date on/after start date") via a form-level superRefine — never the field's own schema, so the isValid oracle (which parses fields in isolation) can't see it. minDateField/maxDateField are rejected together with range: true on the date field itself (validator-enforced); the time equivalents have no such restriction.
- Used by:
date,time
Text
Single-line text input.
| Name | Type | Required | Description |
|---|---|---|---|
| rules | TextRules | Optional | Length/pattern/trim/allow/matches constraints — see the shared TextRules shape below. |
Value: string
{
"type": "text",
"name": "fullName",
"label": "Full name",
"required": true
}Text input with email-format validation.
| Name | Type | Required | Description |
|---|---|---|---|
| rules | TextRules | Optional | Same TextRules shape as text — the email-format check itself is built in, not part of rules. |
Value: string (email-format checked on submit, not blocked while typing)
{
"type": "email",
"name": "email",
"label": "Email address",
"required": true
}Password
Text input with a show/hide toggle.
Optional complexity rules (min length, upper/lower/digit/symbol requirements) via complexity.
| Name | Type | Required | Description |
|---|---|---|---|
| rules | TextRules | Optional | Same TextRules shape as text/email — length/pattern/trim/allow/matches, layered on top of complexity. |
| complexity | PasswordComplexity | Optional | Adds a live pass/fail checklist (uppercase/lowercase/number/special/minLength) under the input while typing; only the currently-failing rules render, and the checklist replaces (never duplicates) the schema's error text. |
Value: string
{
"type": "password",
"name": "password",
"label": "Password",
"required": true,
"complexity": {
"minLength": 8,
"number": true
}
}Textarea
Multi-line text input.
| Name | Type | Required | Description |
|---|---|---|---|
| rules | TextRules | Optional | Same TextRules shape as text — see the shared TextRules shape below. |
Value: string
{
"type": "textarea",
"name": "bio",
"label": "Bio",
"rules": {
"maxLength": 500
}
}Masked
Pattern-masked text input (# digit, A letter, * alphanumeric, other characters literal).
Stores the raw token characters — the mask is presentation-only, not part of the value.
| Name | Type | Required | Description |
|---|---|---|---|
| mask | string | Required | Pattern made of '#' (digit), 'A' (letter), '*' (alphanumeric) token chars — every other character is a literal; must contain at least one token char (validator-enforced). |
| message | string | Optional | Custom error for an incomplete value; defaults to the "Incomplete value" message. |
Value: string — RAW token characters only (e.g. "4111111111111111"), never the punctuated display string
{
"type": "masked",
"name": "ssn",
"label": "SSN",
"mask": "###-##-####"
}Number
Numeric input with optional min, max, and step.
| Name | Type | Required | Description |
|---|---|---|---|
| min | number | Optional | Native min constraint. |
| max | number | Optional | Native max constraint. |
| step | number | Optional | Native step constraint (also the spinner increment). |
Value: number, or undefined once the input is blank/invalid
{
"type": "number",
"name": "age",
"label": "Age",
"min": 0,
"max": 120,
"step": 1
}OTP
One-time-passcode input (shadcn input-otp) with a configurable length.
dependsOn gates it on a sibling field's value and invalidates the verified code when that value changes.
| Name | Type | Required | Description |
|---|---|---|---|
| length | number | Required | Number of code digits/characters rendered as input-otp slots. |
| dependsOn | string | Optional | Sibling field whose value gates this code: changing it invalidates any already-verified code (generation-stamped) and resets the send/verify flow. Rejected inside groups. |
Value: string of exactly length characters; submit gating reads the separate verified-code registry, not this string alone
{
"type": "otp",
"name": "emailOtp",
"label": "Verification code",
"length": 6,
"dependsOn": "email"
}Phone
International phone input (react-phone-number-input) with a country flag dropdown.
countryFrom syncs the selected country from a sibling country or single-select field — the source always wins over a manual pick until it changes again.
| Name | Type | Required | Description |
|---|---|---|---|
| defaultCountry | string (ISO alpha-2) | Optional | Initial calling code shown before the user picks or types a number. |
| preferredCountries | string[] (ISO alpha-2) | Optional | Pins these countries, in this order, above the rest (which stay name-sorted) in the flag dropdown. |
| countryFrom | string | Optional | Sibling country or single-value select field this phone re-syncs its calling code from on every change — see the shared countryFrom shape below. |
Value: string — international phone string (e.g. "+15551234567"), or ""
{
"type": "phone",
"name": "phone",
"label": "Phone number",
"defaultCountry": "US",
"preferredCountries": [
"US",
"CA",
"GB"
],
"countryFrom": "country"
}Select
Dropdown, or a searchable combobox when searchable is set; supports multiple.
optionsFrom can derive the option list from another field's current value instead of a static options array.
| Name | Type | Required | Description |
|---|---|---|---|
| options | Option[] | Optional | Static option list. Exactly one of options or optionsFrom is required — set neither or both and validateFormConfig throws. |
| optionsFrom | { field: string; map: Record<string, Option[]> } | Optional | Derives the option list from a sibling field's current value instead of a static array — see the shared optionsFrom shape below. |
| searchable | boolean | Optional | Renders a filterable combobox instead of the native <select>. |
| multiple | boolean | Optional | Value becomes Option["value"][] instead of a single value; also switches rendering to the combobox (like searchable). |
Value: single (default): the matching Option["value"] (string | number), or undefined. multiple: true → Option["value"][]
{
"type": "select",
"name": "role",
"label": "Role",
"options": [
{
"label": "Admin",
"value": "admin"
},
{
"label": "Member",
"value": "member"
}
]
}Country
Searchable combobox of ISO 3166-1 alpha-2 countries, with flags.
Value is ISO alpha-2 by construction, so it's always a valid phone countryFrom source. Like an optional radio, it can't be cleared once set — the combobox has no clear row.
| Name | Type | Required | Description |
|---|---|---|---|
| countries | string[] (ISO alpha-2) | Optional | Restricts the combobox to this subset; defaults to every ISO 3166-1 alpha-2 country (libphonenumber-js getCountries()). |
| preferredCountries | string[] (ISO alpha-2) | Optional | Pins these countries, in this order, above the rest; must be a subset of countries when both are set (validator-enforced). |
Value: string — ISO 3166-1 alpha-2 code (e.g. "AE"), or undefined
{
"type": "country",
"name": "country",
"label": "Country",
"preferredCountries": [
"US",
"CA",
"GB"
]
}Radio
Exclusive choice from a set of options.
| Name | Type | Required | Description |
|---|---|---|---|
| options | Option[] | Required | Choices rendered as a radix RadioGroup; at least one option is required (validator-enforced). |
Value: the matching Option["value"]
{
"type": "radio",
"name": "plan",
"label": "Plan",
"options": [
{
"label": "Monthly",
"value": "monthly"
},
{
"label": "Yearly",
"value": "yearly"
}
]
}Segmented
Radio semantics (radix RadioGroup) rendered as a joined button group.
An optional segmented field can't be cleared once set — same as radio.
| Name | Type | Required | Description |
|---|---|---|---|
| options | Option[] | Required | Choices rendered as a joined button group with radio semantics (radix RadioGroup, not a toggle group) — guaranteed radiogroup/radio roles and arrow-key roving focus; at least one option is required. |
Value: the matching Option["value"] (radio semantics — same as radio)
{
"type": "segmented",
"name": "size",
"label": "Size",
"options": [
{
"label": "S",
"value": "s"
},
{
"label": "M",
"value": "m"
},
{
"label": "L",
"value": "l"
}
]
}Checkbox
Boolean checkbox, or a checkbox group when options is set.
| Name | Type | Required | Description |
|---|---|---|---|
| options | Option[] | Optional | Presence of options renders a checkbox GROUP (value becomes Option["value"][] of the checked entries); omit it for a single boolean checkbox. |
Value: no options (default): boolean. options set → Option["value"][] of the checked entries
{
"type": "checkbox",
"name": "acceptTerms",
"label": "I accept the terms",
"required": true
}Switch
Boolean toggle switch.
Always a single boolean value — options are not supported (use checkbox for a group).
| Name | Type | Required | Description |
|---|---|---|---|
| options | Option[] | Optional | Inherited from the shared checkbox/switch config shape but NOT functionally supported on switch (known issue) — a switch always renders and stores a single boolean regardless of options; use checkbox for a group. |
Value: boolean — always a single toggle; options has no effect
{
"type": "switch",
"name": "marketingOptIn",
"label": "Send me updates"
}Date
Single date or range picker (react-day-picker).
Value is a plain "yyyy-MM-dd" string (or a {from, to} pair for range), compared by date part — never epoch math.
| Name | Type | Required | Description |
|---|---|---|---|
| range | boolean | Optional | Renders a range calendar and switches the value to { from?; to? } instead of a single "yyyy-MM-dd" string. |
| minDate | "yyyy-MM-dd" | Optional | Earliest selectable date; also biases the calendar's initial month. |
| maxDate | "yyyy-MM-dd" | Optional | Latest selectable date; also biases the calendar's initial month. |
| pickerBounds | "restrict" | "validate" | Optional | What minDate/maxDate do to the calendar — they always constrain the value. "restrict" (default) disables out-of-range days and clamps month navigation, for a range that is genuinely unavailable. "validate" leaves those days selectable by mouse and keyboard so picking one fails with message instead, which is how an age cutoff says "You must be 18 or older" rather than greying out most of the calendar. Governs these two bounds only — minDateField/maxDateField never shape the calendar either way. |
| minDateField | string | Optional | Bounds this date to be on/after a sibling non-range date field's current value (form-level refine, not this field's own schema); rejected together with range: true (validator-enforced). |
| maxDateField | string | Optional | Bounds this date to be on/before a sibling non-range date field's current value; same range: true restriction as minDateField. |
| message | string | Optional | Custom error for a minDate/maxDate violation, replacing "Must be at least/at most <date>" — the way to say "You must be 18 or older" instead of a bare bound. One sentence covers both bounds, and with range: true both endpoints; unparseable values and the minDateField/maxDateField rules keep their own messages. |
Value: single (default): "yyyy-MM-dd" string, or undefined. range: true → { from?: "yyyy-MM-dd"; to?: "yyyy-MM-dd" } (from may be set alone mid-selection)
{
"type": "date",
"name": "birthDate",
"label": "Date of birth",
"maxDate": "2026-07-12"
}Time
Native time input.
Value is a zero-padded "HH:mm" string, compared lexicographically — same convention as dates.
| Name | Type | Required | Description |
|---|---|---|---|
| minTime | "HH:mm" | Optional | Earliest selectable time — zero-padded 24h, compared lexicographically. |
| maxTime | "HH:mm" | Optional | Latest selectable time; must not be before minTime (validator-enforced). |
| stepMinutes | number | Optional | Minute increment; converted to seconds for the native input's step attribute. |
| minTimeField | string | Optional | Bounds this time to be on/after a sibling time field's current value (form-level refine). |
| maxTimeField | string | Optional | Bounds this time to be on/before a sibling time field's current value (form-level refine). |
Value: zero-padded "HH:mm" string, or ""
{
"type": "time",
"name": "appointmentTime",
"label": "Appointment time",
"minTime": "09:00",
"maxTime": "17:00",
"stepMinutes": 15
}Rating
1..max star rating (default max 5).
Clicking the current value clears it when the field is optional.
| Name | Type | Required | Description |
|---|---|---|---|
| max | number | Optional | Highest star value (1..max); defaults to 5 when unset. Must be between 2 and 10 (validator-enforced). |
Value: number 1..max, or undefined (clicking the current value clears it when the field is optional)
{
"type": "rating",
"name": "satisfaction",
"label": "How satisfied are you?",
"max": 5
}Slider
Numeric slider (shadcn Slider) with min, max, and step.
| Name | Type | Required | Description |
|---|---|---|---|
| min | number | Required | Lower bound of the range. |
| max | number | Required | Upper bound of the range. |
| step | number | Optional | Increment between selectable values. |
Value: number
{
"type": "slider",
"name": "volume",
"label": "Volume",
"min": 0,
"max": 100,
"step": 5
}Signature
Draw-to-sign canvas (signature_pad); value is a PNG data URL.
Not keyboard-accessible — drawing is inherently pointer/touch-only, with no keyboard fallback.
| Name | Type | Required | Description |
|---|---|---|---|
| penColor | string (CSS color) | Optional | Ink color; defaults to signature_pad's own default (black). Treated as static config — changing it at runtime recreates the pad and only restores the value captured at mount. |
| heightPx | number | Optional | Canvas height in pixels; defaults to 160. |
Value: PNG data URL string, or "" until the user signs
{
"type": "signature",
"name": "signature",
"label": "Signature",
"heightPx": 180
}File
Drag-and-drop or browse, with a per-file rejection reason; accept, maxSizeMB, and multiple.
The field holds File objects and stops there — uploading them is the host's job, in onSubmit. There is no progress bar because the engine never transfers anything.
| Name | Type | Required | Description |
|---|---|---|---|
| accept | string | Optional | Native accept attribute (MIME types / extensions), e.g. ".pdf,.doc,.docx". Also enforced by the generated schema — a file the browser could not type matches no MIME token, so list the extension too (".pdf,application/pdf") if those must get through — and written out as the dropzone's hint ("PDF, DOC or DOCX"). |
| maxSizeMB | number | Optional | Rejects files larger than this via the generated schema (a field error, not a manual setError, so it survives the next resolver run); unset means no size limit. |
| multiple | boolean | Optional | Value becomes File[] instead of a single File; also allows picking more than one file at once. |
Value: single (default): a File, or undefined — not JSON-serializable, consumers handle upload in onSubmit. multiple: true → File[]. Files that failed accept or maxSizeMB are in there too: they stay in the value so the field can show each one its own reason, and the field is invalid until they are removed — so onSubmit never sees them, but a manual read of the value will.
{
"type": "file",
"name": "resume",
"label": "Resume",
"accept": ".pdf,.doc,.docx",
"maxSizeMB": 5
}Hidden
Not rendered; carries a static value along with the rest of the form's values.
| Name | Type | Required | Description |
|---|---|---|---|
| value | unknown | Required | The static value carried through with the rest of the form's values; must not appear in a step's fieldNames (validator-enforced — it's exempt from step assignment). |
Value: config.value verbatim (unknown) — carried through untouched
{
"type": "hidden",
"name": "referralSource",
"value": "landing-page"
}Static text
Non-field content block — heading, paragraph, or divider. Has no value.
| Name | Type | Required | Description |
|---|---|---|---|
| content | string | Required | The block's text; may contain an allowlisted inline <a href> or <br> (safe schemes only) via the field's rich-text renderer. |
| as | "h1" | "h2" | "p" | "divider" | Optional | Element to render as; omit for a plain paragraph. "divider" ignores content and renders a separator instead. |
Value: no value — not a form field; renders content only
{
"type": "static",
"name": "sectionIntro",
"as": "h2",
"content": "Contact details"
}Group (repeatable)
Repeatable sub-array of fields (e.g. "add another team member"), with min/max.
Known v1 limitation: visibleWhen/disabledWhen conditions on fields nested inside a group are not skipped by validation.
| Name | Type | Required | Description |
|---|---|---|---|
| fields | AnyFieldConfig[] | Required | Recursive — any field type documented on this page (not expanded here to avoid infinite recursion in this table). Known v1 limitation: visibleWhen/disabledWhen conditions on fields nested inside a group are NOT skipped by validation. |
| min | number | Optional | Minimum rows; defaults to 0. The remove button disables once row count reaches this floor. |
| max | number | Optional | Maximum rows; defaults to unbounded. The add button disables once row count reaches this ceiling. |
Value: an array of row objects, one per repetition, each shaped by fields (bounded by min/max)
{
"type": "group",
"name": "teamMembers",
"label": "Team members",
"min": 1,
"max": 5,
"fields": [
{
"type": "text",
"name": "name",
"label": "Name",
"required": true
}
]
}Submit button
Submit button with configurable text and variant.
| Name | Type | Required | Description |
|---|---|---|---|
| text | string | Required | Button label. |
| variant | ButtonVariant | Optional | 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link' (shadcn Button variant). |
Value: no value — renders a submit button
{
"type": "submit",
"name": "submit",
"text": "Create account",
"variant": "default"
}For the exact per-type config shape (which properties each type accepts), see the FieldConfig union in form-builder/core/types.ts, or a working config for several of these under Examples.