Themes
Five starters and your own references to start from. Each form keeps its own copy of its look.
A theme is a visual identity for forms: tokens (colors, fonts, radius,
spacing), optional CSS and components, and a THEME.md that tells
people and agents when to use it.
Themes are references, and each form owns its look. The themes in your
project's themes/ folder are what you and your agent start from when making a
form. Using one copies it into the form's folder, and from then on that copy is
the form's look and no one else's.
themes/
└── brand/ # a reference for new forms
├── index.tsx # defineTheme({ colors, font, …, components })
├── brand.css # optional, scoped to [data-of-theme="brand"]
└── THEME.md # palette, type, components, signature ideas, copy tone, do / don't
forms/
└── meetup/
├── index.tsx # import theme from './theme/index.tsx'
└── theme/ # the form's own copy: only this form uses it
├── index.tsx
└── brand.css # rescoped to [data-of-theme="meetup"]A theme with tokens only can be index.ts, or a single file themes/<id>.ts.
Use index.tsx once it adds components. A form without a theme of its own gets
the plain base styles. The classic starter spells them out token by token, so
open-form theme copy classic <form> gives a form that same look to tweak.
Using a theme in a form
Copy a theme into the form:
pnpm exec open-form theme copy terminal meetuptheme copy <theme> <form> takes the theme from themes/ when the project has
one with that id, and otherwise the starter of that name. It copies the
theme's files (index.ts or index.tsx, CSS, fonts/) into
forms/<form>/theme/, leaving THEME.md behind with the reference, and
rewrites [data-of-theme="<theme>"] selectors to the form id. It won't replace
a form's existing theme/ folder unless you pass --force.
Then apply it in the form. Import the theme above the form's own stylesheet, so the form's CSS wins over the theme's:
import { defineForm, Form } from '@open-form-build/core';
import theme from './theme/index.tsx';
import './meetup.css';
export const config = defineForm({ title: 'Developer Meetup' });
export default function Meetup() {
return <Form theme={theme}>…</Form>;
}theme copy prints these two lines when the form doesn't have them yet, with
the copy's own file name: a tokens-only theme such as classic is copied as
index.ts, so its import is ./theme/index.ts. For a new form, create does
both steps at once:
pnpm exec open-form create meetup --theme terminalThe form's root carries data-of-theme="<form-id>", so the copy's CSS applies
to this form only. The theme's components replace the defaults, and
<Form components> wins over the
theme's.
Changing a form's look
Edit forms/<id>/theme/ by hand or ask your agent: tokens, CSS, components,
fonts. Only that form changes. Editing or deleting a theme in themes/ never
changes a form that already has a copy, so a brand refresh reaches existing
forms only when you edit their copies or copy the theme in again with
--force, which replaces the form's copy along with any edits made to it.
- Renaming a form folder changes its id, so update the
[data-of-theme="…"]selectors in its theme CSS to match. - A form stands on its own.
open-form checkwarns when a form imports fromthemes/or from another form's folder; copy what it needs instead. - Component classes are global. A theme's own classes, such as the terminal
starter's
.tm-*, aren't scoped to the form. If you embed several forms on one page and their copies of the same starter have diverged, rename the prefix in one of them.
Studio has no per-form look editor yet: its Adjust drawer edits the
references in themes/.
References and starters
themes/ holds the references your agent reads when it creates a form: the
project's brand theme, a starter you've adapted, a look made with
/create-theme. A new workspace starts with an
empty themes/, and nothing renders from it in production.
Open Form ships five starters. Copy one into a form as it is, or into themes/
to adapt it first, the same way shadcn/ui components work.
| Starter | Feel | Layout | Good for |
|---|---|---|---|
classic | Clean neutral card, every token spelled out | card | Product forms, internal requests, a safe default |
editorial | Serif magazine, underline inputs, ink panel | SplitLayout | Agencies, B2B sales, applications, premium brands |
terminal | Monospace CLI session, green on black, ? prompts | terminal window | Developer events, devtools, hackathons, beta access |
brutalist | Loud yellow, thick borders, hard shadows | card | Launches, waitlists, campaigns |
conversational | One question per screen, rounded, soft violet, letter keys | FocusLayout | Surveys, NPS, quizzes, onboarding, RSVPs |
pnpm exec open-form theme list # the project's themes, then the starters
pnpm exec open-form theme add editorial --as brand # → themes/brand/, a reference to adapt
pnpm exec open-form theme copy brand meetup # → forms/meetup/theme/theme add --as renames the starter's copy: the data-of-theme selectors in its
CSS and the title in its THEME.md follow the new id, and --force replaces a
theme that already has that id. Changing a reference affects only forms you copy
it into later.
Tokens and components
Tokens recolor and reshape: colors and a dark palette, font,
headingFont, radius, buttonRadius, spacing, width, layout,
colorScheme. They cover most brand themes; pill buttons are just
buttonRadius: '999px'.
Components change how the form feels. Built-in fields only provide
behavior, and their markup comes from replaceable components (Field,
TextInput, Select, ChoiceGroup, …) and chrome (Layout, Header,
Progress, Nav, Success, Notice, …). Replace any subset and the rest keep
the default look. Validation, focus and the server's view of your fields don't
depend on them.
See defineTheme for every token and
Layouts for the built-in layouts and chrome.
THEME.md
Each reference has a THEME.md with its mood, what it suits and when to avoid
it, its palette, typography, layout and components, signature ideas (a few
form-specific elements that fit the theme, such as terminal's command that fills
in as people answer), copy tone, and do / don't. The
create-form skill reads these to propose visual
directions before it writes a form, and Studio renders it on the theme's page.
It stays with the reference; a form's copy doesn't need one.
In Studio
/studio/themes is a gallery of the project's references: a live thumbnail of a
sample form in each theme, its name and one-line description, and a badge on
themes without a THEME.md. New theme adds a starter to themes/ in one
click, or turns a description into a prompt to copy to your agent.
A theme's page shows a large preview of the sample form next to its THEME.md
and a spec drawn from its tokens: palette, contrast report, type samples and
shape. Switch the color scheme and the device, and jump to any page or the done
screen. The Use it in a form card shows the open-form theme copy command
and a prompt to copy to your agent for a new form in this theme.
Adjust opens a drawer. Quick tweaks cover the primary and background
colors, body and heading fonts, radius and color scheme, with a contrast check.
All tokens has every color in the light and dark palettes, dark mode, fonts,
radius, button radius, width, spacing and frame. Save to source writes the
tokens straight into the file the reference lives in (index.ts, index.tsx
or a single-file themes/<id>.ts), so only forms copied from it later get the
change. For anything bigger, describe the change and copy the prompt to your
agent.
The Themes pages and Adjust are available under open-form dev only. Keep
tokens as plain literals inside defineTheme() so Studio can edit them; a value
computed from a variable is left for you to change by hand.
Web fonts
In Adjust, the body and heading font menus have Add from Google Fonts….
Search by name, filter by category (including Chinese, Japanese and Korean), and
preview your own text. Picking a family downloads it into the theme and selects
it. open-form theme font does the same from the command line, for a reference
or, with --form, for a form's own copy:
pnpm exec open-form theme font brand Inter # → themes/brand/fonts/
pnpm exec open-form theme font meetup Inter --form # → forms/meetup/theme/fonts/forms/meetup/theme/
├── index.tsx # import './fonts/inter.css'
└── fonts/
├── inter.css # @font-face rules
└── inter/ # woff2 files, split by character rangeThe form serves these files itself, so respondents never contact Google.
Linking fonts.googleapis.com instead would send every respondent's IP address
to Google. For a font that isn't on Google Fonts, copy its .woff2 files into
fonts/ and declare @font-face in the theme's CSS.
Check contrast
pnpm exec open-form theme check brand # a reference in themes/
pnpm exec open-form theme check meetup --form # a form's own copytheme check reports WCAG contrast for every color pair forms actually use, in
light and dark, and exits non-zero when one fails. It reads hex, rgb(), hsl(),
oklch() and oklab(). Values it can't resolve, such as var() and
color-mix(), show as ? and count as failures. Fix a failure by adjusting the
token: keep the hue, change the lightness.
Create your own
Ask your agent to run /create-theme. It reads your
repo's design system (Tailwind, CSS variables, shadcn, fonts), screenshots or a
description, starts from the closest starter, writes tokens, CSS, components and
THEME.md into themes/<id>/, and keeps theme check passing. Forms then use
it through theme copy.
/create-theme make it look like our product