Open Form

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 meetup

theme 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:

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

The 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 check warns when a form imports from themes/ 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.

StarterFeelLayoutGood for
classicClean neutral card, every token spelled outcardProduct forms, internal requests, a safe default
editorialSerif magazine, underline inputs, ink panelSplitLayoutAgencies, B2B sales, applications, premium brands
terminalMonospace CLI session, green on black, ? promptsterminal windowDeveloper events, devtools, hackathons, beta access
brutalistLoud yellow, thick borders, hard shadowscardLaunches, waitlists, campaigns
conversationalOne question per screen, rounded, soft violet, letter keysFocusLayoutSurveys, 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 range

The 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 copy

theme 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

What's next

On this page