Fields
Every built-in field, from text and choices to ratings, matrices and phone numbers: what each one stores and how it's validated on both sides.
Fields are behavior: each one wires a value into the form's state,
validation, focus and storage. How they look comes from the
theme's components, so the same <Select> can
render as a native select, chips or a terminal prompt.
<Text name="name" label="Name" required autoComplete="name" />
<Email name="email" label="Email" description="We'll send the ticket here." required />
<NumberInput name="age" label="Age" min={13} max={120} integer unit="years" />
<RadioGroup name="plan" label="Plan" options={['Free', 'Pro']} variant="cards" required />
<Checkbox name="terms" required>I agree to the code of conduct</Checkbox>Shared props
Prop
Type
A field's defaultValue becomes its answer when the field first shows. Until
its step has been shown, useFieldValue, useFormValues, <When> and step
skipping see no answer, so put answers they need from the start in
<Form defaultValues>.
Built-in fields
| Component | Stored as | Extra props |
|---|---|---|
<Text> | string | placeholder, minLength, maxLength, pattern, patternMessage, autoComplete |
<Email> | string | Same as Text; checks the email format |
<Url> | string | Same as Text; requires http:// or https:// |
<Tel> | string | Same as Text; loose phone format. With defaultCountry or countries, a country picker and +886912345678 answers (Phone numbers) |
<Textarea> | string | Same as Text, plus rows (default 4); shows a counter when maxLength is set |
<NumberInput> | number | min, max, step, integer, unit (a suffix like "years"), placeholder |
<DateInput> | YYYY-MM-DD string | min, max as YYYY-MM-DD |
<Select> | string | options, placeholder |
<RadioGroup> | string | options, variant |
<CheckboxGroup> | string[] | options, minSelected, maxSelected, variant |
<Checkbox> | boolean | Children are the text beside the box |
<Rating> | number | min (default 1), max (default 5), lowLabel, highLabel |
<Matrix> | One string per row | rows, columns, label, description, required (Matrix) |
Text answers are trimmed before they're validated and stored, and a blank
answer counts as no answer.
CheckboxGroup stores the selected values in option order. Rating stores a
whole number; use min={0} max={10} for an NPS question.
A Checkbox without label uses its children as the column name in
Responses. Give it both a label and children to show a question above the
box.
Options
options takes plain values or value/label pairs:
<Select name="role" label="Role" options={['Student', 'Engineer', 'Designer']} />
<RadioGroup
name="plan"
label="Plan"
options={[
{ value: 'free', label: 'Free — for side projects' },
{ value: 'pro', label: 'Pro — for teams' },
]}
/>Values are stored as strings. Use value/label pairs when labels are long or
may change, and on translated forms: the
label is what people read, the value is what's stored and what
<When> compares against. Answers outside the option
list are rejected on both sides, so the list must be known when the form
renders: a choice field with no options fails to load.
variant on RadioGroup and CheckboxGroup is 'list' (default, stacked)
or 'cards' (a responsive grid of tiles). It's a hint: a theme's own
ChoiceGroup may render choices its own way. Prefer RadioGroup for five or
fewer options and Select for longer lists.
Matrix
A matrix asks several single-choice questions that share one set of answers — rate each part of a course, say — and lays them out as a grid: the answers head the columns once, each row is a question.
<Matrix
label="Rate the course"
columns={['Poor', 'Fair', 'Good', 'Great']}
rows={[
{ name: 'content', label: 'Content' },
{ name: 'teacher', label: 'Teacher' },
{ name: 'venue', label: 'Venue', required: false },
]}
required
/>| Prop | Type | Description |
|---|---|---|
columns | OptionInput[] | The answers every row offers, like options. |
rows | { name, label, required? }[] | One question per row. name is its key, like any field's. |
required | boolean | Every row must be answered; a row's own required overrides it. |
label, description | string | The question above the grid. |
Each row is an ordinary single-choice field, exactly as if you'd written a
RadioGroup with the columns as its options: it's stored under its own
name, gets its own column in Responses and exports, can be named in
<When> (<When field="venue" equals="Poor">), and
shows its own error. The row's label is its column heading, so make it
readable on its own.
The shared props don't apply to a matrix: it has no name,
defaultValue or validate of its own, and rows take only name, label
and required. To start a row with an answer, pass it in
<Form defaultValues> under the row's name.
On a wide form the grid has a column per answer; on a narrow one, or a phone,
each row becomes a card listing its answers. Prefer a matrix to a run of
RadioGroups when three or more questions share the same answers.
Phone numbers
A plain <Tel> is one box that takes any phone-like answer, stored as typed.
Give it defaultCountry or countries and it becomes two: a country picker
and the number.
The answer is stored as one international number, so Responses and exports get
a single, consistent column:
<Tel name="phone" label="Mobile" defaultCountry="TW" required />
<Tel name="phone" label="Mobile" countries={['TW', 'HK', 'JP']} />| Prop | Type | Description |
|---|---|---|
defaultCountry | PhoneCountry | The country the picker starts on, as an ISO 3166-1 alpha-2 code: 'TW', 'US', 'GB'. With countries it must be one of them; without it, the picker starts on the first of countries. |
countries | PhoneCountry[] | The countries in the picker, in this order. Numbers without one of their country codes are rejected. Without it the picker lists every country, sorted by name. |
An unknown country code, an empty countries list, or a defaultCountry that
isn't in countries is a mistake in the form,
so the form fails to load with an error naming the field, as open-form check
shows.
- Someone in Taiwan who picks
Taiwan +886and types0912-345-678is stored as+886912345678: spaces, dashes and brackets go, and so does the leading0people dial at home. Where a leading 0 belongs to the number (Italy), it stays. - An answer typed with its own code, such as a pasted
+44 20 7946 0958or0044 20 7946 0958(00is read as+), keeps that code. Once the respondent leaves the box, the picker switches to that country if it's one ofcountries; otherwise the answer is rejected. Picking another country afterwards replaces the code, so the picker always shows the stored one. - Country names follow the form's language.
- The check is the international format:
+, a country code and up to 15 digits. Per-country lengths aren't checked, so real numbers are never turned away; usepatternfor a stricter rule. Withcountries, the country code must be one of theirs. Countries that share a code, like the US and Canada, accept each other's numbers. pattern,minLengthandmaxLengthtest the stored international number, not what was typed:pattern="^\+8869\d{8}$"accepts only Taiwanese mobiles.defaultValue, and a<Form defaultValues>entry, can be international ('+886912345678') or national ('0912345678'): a national one is stored with the starting country's code, like a typed answer.- The number box autocompletes as
tel-national; passautoCompleteto change it.
The picker and the box are the theme's own Select and TextInput, so they
match any theme. To draw them differently, replace
PhoneInput.
Autocomplete
Set autoComplete on personal fields so browsers can fill them: name,
given-name, family-name, organization, street-address, postal-code,
country-name. Email, Url and Tel set email, url and tel for you.
Validation
The validation props — required, minLength / maxLength, pattern,
min / max / integer / step, minSelected / maxSelected and date bounds —
plus the format checks on Email, Url and Tel and option membership on
choice fields are one rule set. The browser and the server run the same code,
so what the form accepts is what the server stores.
patternmust match the whole value, like the HTML attribute. Pair it withpatternMessagefor a readable error. A field with apatternand nomaxLengthaccepts up to 1,000 characters, so a slow pattern can't be turned against the server with a huge answer.- Patterns run in Unicode mode when they can; ones only older syntax accepts,
such as
\d{3}\-\d{4}or[\w-.]+, run without it. A pattern that isn't a regular expression at all fails to load, andopen-form checknames it. steponNumberInputacceptsmin(or 0 without amin) plus a whole number of steps:min={1} step={0.5}takes 1, 1.5, 2…Emailaccepts at most 254 characters, the longest address mail allows.minLengthandmaxLengthcount characters, not bytes.- A checkbox group stores each picked value once.
- Built-in error messages appear in the language the form is shown in.
Custom rules with validate
validate takes a function that returns an error message (or nothing), or any
Standard Schema — Zod, Valibot, ArkType — if the
project already has one installed.
import { z } from 'zod';
const workEmail = (value: unknown) =>
/@(gmail|yahoo|hotmail|outlook)\./i.test(String(value))
? 'Please use your work email.'
: undefined;
<Email name="email" label="Work email" required validate={workEmail} />
<Text name="handle" label="GitHub handle" validate={z.string().regex(/^[\w-]+$/, 'Letters, numbers and dashes only.')} />- A function receives
(value, allValues)and may be async.allValuesholds the answers of the fields that are showing, trimmed and typed as they're stored — the same object on both sides. - It runs only when the field has an answer and the built-in rules passed.
- It runs in the browser and again on the server, so keep it free of browser APIs. Don't call other services from a validator.
Why NumberInput and DateInput
import { Number, Date } from '@open-form-build/core' would shadow the JavaScript globals
in the same file, and validators often call Number(value) or new Date().
The Input suffix keeps both available.