Open Form

Themes

Five starters and your own references to start from. Each form's look is written for it, with only what it uses.

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. As in open-slide, the themes in your project's themes/ folder are what you and your agent read when making a form. No form imports one or copies it whole: a new form starts from a theme's tokens, and the rest of its look is written for it in its own theme/ folder, with only the parts that form uses.

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, tokens, components, signature ideas, copy tone, do / don't
forms/
└── meetup/
    ├── index.tsx         # import theme from './theme/index.ts'
    └── theme/            # the form's own look: only this form uses it
        ├── index.ts      # its tokens (index.tsx once it has components)
        └── theme.css     # the CSS it needs, scoped to [data-of-theme="meetup"]

Every form made with open-form create has a theme/index.ts. Empty, defineTheme({}) is the plain base look, which the classic starter spells out token by token. A form without a theme/ folder at all gets the same look.

Starting a form from a theme

npx open-form create meetup --theme terminal

--theme takes the theme from themes/ when the project has one with that id, and otherwise the starter of that name. It writes the theme's tokens into the new form's theme/index.ts: colors and the dark palette, fonts, radius, width, spacing, color scheme. Web fonts those tokens name come along into theme/fonts/. The theme's CSS and components stay with the reference.

forms/meetup/theme/index.ts
import { defineTheme } from '@open-form-build/core';

export default defineTheme({
  font: 'ui-monospace, "SF Mono", …, monospace',
  radius: '4px',
  colorScheme: 'dark',
  colors: {
    primary: '#5af78e',
    // …
  },
});

The form imports it above its 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.ts';
import './meetup.css';

export const config = defineForm({ title: 'Developer Meetup' });

export default function Meetup() {
  return <Form theme={theme}>…</Form>;
}

The form's root carries data-of-theme="<form-id>", so the form's theme CSS applies to this form only. The theme's components replace the defaults, and <Form components> wins over the theme's.

Writing the rest of the look

Your agent writes the rest of the form's look in its theme/ folder, reading the reference's THEME.md and its code, and taking only what the form renders. It works in this order, moving on only when the step before can't give the look:

  1. Tokens in theme/index.ts, including a built-in layout: components: { Layout: SplitLayout }.
  2. CSS on the runtime's own markup, scoped to [data-of-theme="<form-id>"]: label and help treatment, pill or list-row choices, button glyphs and keycaps drawn with ::before and ::after, radios restyled as ticks. See Theme CSS for the classes.
  3. Components only for markup or behavior CSS can't give: a window frame, a wrapper an input needs for its underline or its ❯ prompt, keyboard shortcuts, auto-advance.

So a form in the terminal look carries the window, the ASCII progress bar and the ❯ input lines as components, and draws its ? prompts, ( ) marks and register ↵ button in CSS. A field type it doesn't use, such as a Select, isn't in its theme at all (open-form check warns when one is), and the starters' later improvements never have to be copied into it.

Changing a form's look

Edit forms/<id>/theme/ by hand or ask your agent: tokens, CSS, components, fonts. Only that form changes. In Studio, the form's preview has an Adjust mode for its colors, fonts and shape. Editing or deleting a theme in themes/ never changes a form.

  • 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.
  • Component classes are global. Classes a form took from a reference, such as the terminal starter's .tm-*, aren't scoped to the form. If you embed several forms on one page and two of them style the same class differently, rename the prefix in one of them.

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/. No form renders from it: a production build carries the references only for Studio's read-only Themes pages.

Open Form ships five starters. Start a form from one, or add one to 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
npx open-form theme list                      # the project's themes, then the starters
npx open-form theme add editorial --as brand  # → themes/brand/, a reference to adapt
npx open-form create meetup --theme brand     # a new form from brand's tokens

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 start from it 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 and every token, typography, layout and components (which parts are CSS and which need a component), 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 and to write a form's look, and Studio renders it on the theme's page.

In Studio

/studio/themes is a read-only 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. It's part of the production build too, so a deployed Studio shows the same gallery. New theme turns a description into a prompt to copy to your agent, and shows the open-form theme add <starter> command for starting from a starter.

A theme's page opens with a large preview of the sample form: switch the color scheme and the device, and jump to any page or the done screen. Below it, Tokens lays out what a form started with --theme <id> copies: the palette in each scheme with a contrast verdict (Details opens every pair and its ratio), type samples, and shape (radius, width, spacing and frame) drawn in the theme's own colors. Then comes its THEME.md, with an outline of its sections to jump between, beside two cards:

  • Use it in a form: the open-form create <form> --theme command and a prompt for your agent to start a new form in this theme.
  • Change this theme: describe the change and copy it as a prompt. Your agent edits the tokens, CSS, components and THEME.md and keeps contrast passing.

Studio never writes a reference theme, so changing one is always your agent's or your own edit, and forms already made from it keep their own look. A reference that fails to load says why on its page instead of showing the default look.

Adjust a form's look

A form's Preview tab has an Adjust mode next to Preview and Edit. It opens the same panel on the form's own theme/index.ts: change colors, fonts and shape and the preview beside it follows before you save, showing the palette you're editing. Save to source writes the tokens into the form's theme file, and fonts added from Google Fonts go into its theme/fonts/. A form without a theme/ folder gets a Create look file button, which writes theme/index.ts and passes it to the form's <Form>. A <Form> that already gets another theme (theme={brand}) is left alone: move what that theme sets into the form's own theme/ first. A look file the form doesn't import is reported by open-form check.

Adjust is 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 form's theme/fonts/ 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 look:

npx open-form theme font brand Inter          # → themes/brand/fonts/
npx open-form theme font meetup Inter --form  # → forms/meetup/theme/fonts/
forms/meetup/theme/
├── index.ts               # 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

npx open-form theme check brand          # a reference in themes/
npx open-form theme check meetup --form  # a form's own look

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. New forms then start from it with create --theme.

/create-theme make it look like our product

What's next

On this page