Open Form

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

ComponentStored asExtra props
<Text>stringplaceholder, minLength, maxLength, pattern, patternMessage, autoComplete
<Email>stringSame as Text; checks the email format
<Url>stringSame as Text; requires http:// or https://
<Tel>stringSame as Text; loose phone format. With defaultCountry or countries, a country picker and +886912345678 answers (Phone numbers)
<Textarea>stringSame as Text, plus rows (default 4); shows a counter when maxLength is set
<NumberInput>numbermin, max, step, integer, unit (a suffix like "years"), placeholder
<DateInput>YYYY-MM-DD stringmin, max as YYYY-MM-DD
<Select>stringoptions, placeholder
<RadioGroup>stringoptions, variant
<CheckboxGroup>string[]options, minSelected, maxSelected, variant
<Checkbox>booleanChildren are the text beside the box
<Rating>numbermin (default 1), max (default 5), lowLabel, highLabel
<Matrix>One string per rowrows, 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
/>
PropTypeDescription
columnsOptionInput[]The answers every row offers, like options.
rows{ name, label, required? }[]One question per row. name is its key, like any field's.
requiredbooleanEvery row must be answered; a row's own required overrides it.
label, descriptionstringThe 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']} />
PropTypeDescription
defaultCountryPhoneCountryThe 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.
countriesPhoneCountry[]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 +886 and types 0912-345-678 is stored as +886912345678: spaces, dashes and brackets go, and so does the leading 0 people 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 0958 or 0044 20 7946 0958 (00 is read as +), keeps that code. Once the respondent leaves the box, the picker switches to that country if it's one of countries; 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; use pattern for a stricter rule. With countries, the country code must be one of theirs. Countries that share a code, like the US and Canada, accept each other's numbers.
  • pattern, minLength and maxLength test 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; pass autoComplete to 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.

  • pattern must match the whole value, like the HTML attribute. Pair it with patternMessage for a readable error. A field with a pattern and no maxLength accepts 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, and open-form check names it.
  • step on NumberInput accepts min (or 0 without a min) plus a whole number of steps: min={1} step={0.5} takes 1, 1.5, 2…
  • Email accepts at most 254 characters, the longest address mail allows.
  • minLength and maxLength count 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.

forms/meetup/index.tsx
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. allValues holds 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.

On this page