Form
The root component, the form file contract, Submit and Success.
<Form> is the root of every form. A form is one file — forms/<id>/index.tsx
— whose default export renders exactly one <Form>. The folder name is the
form's id and its public URL: /f/<id>.
import { defineForm, Email, Form, Page, Select, Submit, Text, When } from '@open-form-build/core';
export const config = defineForm({
title: 'Developer Meetup Registration',
submissions: { maxResponses: 100, uniqueBy: 'email' },
});
export default function MeetupForm() {
return (
<Form>
<Page title="About you">
<Text name="name" label="Name" required autoComplete="name" />
<Email name="email" label="Email" required />
</Page>
<Page title="Background">
<Select name="role" label="Role" options={['Student', 'Engineer', 'Designer']} required />
<When field="role" equals="Student">
<Text name="school" label="School" required />
</When>
</Page>
<Submit>Register</Submit>
</Form>
);
}File contract
forms/<id>/index.tsxis required. Use a kebab-case id. Anything else in the folder — local components, option lists, a stylesheet,locales/*.json, the form's owntheme/— is yours to import.export const config = defineForm({...})holds the title, description and everything the server enforces: submission limits, dates, protection, the success screen. SeedefineForm.- The default export is a component that renders one
<Form>. Inside it, write any React you like — your own components,.map(), wrappers — as long as the inputs are field components oruseField. - Don't name the default export like an Open Form component —
Form,Email,Text— or a global likeDateorNumber. A component's name would make the form render itself, soopen-form check, dev and build refuse it and name the file. A global's name isn't caught, but it shadows the realDateorNumberin that file.open-form createaddsFormto the name (meetupbecomesMeetupForm) so this can't happen. config.titleandconfig.descriptionrender as the form's header. Don't repeat them in the JSX.
Render-to-collect
The server never parses your file. It renders the default export once in a
collect mode: every <Page> renders, every <When> renders its children and
records its condition, and every field registers its name, type, constraints,
step and conditions. The result is the form's schema — what the server
validates against, what versions are recorded under, and what Responses and
exports use as columns. Custom validate functions are collected too, so the
server runs them.
Because of that:
- Don't touch
window,documentorlocalStorageduring render. Effects are fine. - Keep field names stable. Never derive them from
Date.now()or random values.__proto__isn't allowed as a name. - A
namemay appear twice only with the same field type, in<When>branches that don't show together — for example a postal code with onevalidateper country. Each copy'svalidateapplies while that copy is showing. Two types under one name, or two copies under the same conditions (say, on two pages), is a load error. - The browser repeats this render once when the form loads, off-screen, so it decides what's showing exactly as the server will. Effects in your own components run (and clean up) once more because of it.
- A default export that renders no
<Form>, or more than one, fails to load with an error that names the file.
Props
Prop
Type
aside is for live previews, summaries and prices — see
Hooks. components is covered in
Layouts.
Leave onSubmit out in an Open Form workspace. Submission, server
validation, protection and storage are built in. onSubmit receives the
answers the server would store — fields that are showing, trimmed and typed —
and skips all of that.
Theme
A form owns its look. open-form theme copy <theme> <id> copies a theme into
forms/<id>/theme/ and prints the line that imports it;
open-form create <id> --theme <theme> does both for a new form. The copy
keeps the theme's file type — theme/index.ts for a tokens-only theme like
classic, theme/index.tsx for one with components — so import the path
the command prints, and pass it to theme:
import theme from './theme/index.tsx'; // above the form's own CSS, so the form's CSS wins
import './meetup.css';
<Form theme={theme}>…</Form>The theme's tokens become the form's --of-* variables, and its components
replace the defaults: defaultComponents ← the theme's components ←
<Form components>. The root element gets data-of-theme="<id>", so the
theme's CSS is scoped to this form. Without theme, the form uses the base
styles, which are the classic starter's look — or, under a
<FormHost theme>, that fallback theme. To adjust the
default look, copy it in with open-form theme copy classic <id>. Editing a
copy changes only this form; themes in themes/ are references and never
change it. See Themes and
defineTheme.
For a form-specific tweak on top of the theme, import a local stylesheet after
it and scope it with className. Prefix every class and build on the --of-*
tokens so the theme and dark mode keep working:
import theme from './theme/index.ts'; // a copy of classic
import './pass.css';
<Form theme={theme} className="pass-root">…</Form><Submit>
Sets the label of the final button. It renders nothing itself; Back and Next are automatic. Without it the button reads "Submit" in the language the form is shown in.
<Submit>Register</Submit>Put it directly inside <Form>. One placed inside the last <Page> is found
too.
<Success>
Replaces the thank-you screen shown after a successful submission.
<Success title="You're in">
<p>We'll email your ticket before the event.</p>
</Success>The title comes from <Success title>, else config.success.title, else a
built-in sentence. The body comes from its children, else
config.success.message, else a built-in sentence. Children can read answers
with useFieldValue to personalise the screen. Set config.success.redirect
to send respondents to another page after they submit.