/create-theme
Turns your design system, a screenshot or a brief into a reference theme for new forms.
/create-theme builds a theme folder under themes/<id>/: a reference that new
forms take their look from. Reach for it with "make it look like our product", a
brand or a mood. /create-form hands off to it when
the direction you picked needs a theme the project doesn't have. For a tweak to
a single form, just ask for the tweak — it goes in that form's own
forms/<id>/theme/.
Make our forms look like the product. The design tokens are in apps/web.Sources
Any mix of:
- The repo's design system — the most common. The agent looks before it
asks: Tailwind config or a v4
@themeblock, CSS variables (shadcn's:rootand.dark), theme objects and token files,next/fontcalls and@font-face, logo fills andtheme_color, and the shape of the product's own inputs and buttons. - Images — screenshots, a mood board, a brand guide.
- A description — "dark, neon, arcade", "calm clinic", "Swiss poster".
- A starter to adapt — "editorial, but with our colors".
If the source isn't clear, it asks once. When sources disagree, it asks which
one wins. From images or a description, it gives the theme one distinctive move
— a display heading font, a textured brand panel, an unusual control shape, or
a composition that isn't a centered card, such as a full-page poster or a
document sheet — taken from the audience's world rather than a generated
default, and keeps everything else quiet. width follows that
composition instead of staying at the default by habit.
Token mapping
shadcn and Tailwind token names don't line up with Open Form's. The skill carries this table, traps included:
| Product token | Open Form token | Note |
|---|---|---|
--background | background | The page behind the form |
--card | surface | The form card |
--foreground | foreground | |
--muted-foreground | muted | shadcn --muted is a background, not this |
--border | border | Decorative edges only |
--input | control | shadcn --input is a border color; Open Form input is the field background |
--primary / --primary-foreground | primary / primaryForeground | |
--destructive | error | |
--ring | — | Focus uses primary |
| — | success | The brand's green, or one that passes 3:1 on surface |
--radius | radius | rounded-full buttons become buttonRadius: '999px' |
Colors can be pasted as they are: hex, hsl() and oklch() all work. Tokens
avoid var() and color-mix(), which the contrast check can't read.
What it writes
The agent starts from the closest starter, under the new id:
npx open-form theme add classic --as acmeclassic for product UI, editorial for premium serif, terminal for
developer tools, brutalist for loud campaigns, conversational for one
question per screen. --as renames the [data-of-theme] selectors and the
name in THEME.md; file names and class prefixes stay the starter's, so
theme add editorial --as acme gives themes/acme/editorial.css with .ed-…
classes.
Then it goes only as deep as the look needs:
- Tokens — colors, fonts, radius, spacing, width and layout. Most product
themes stop here; pill buttons are just
buttonRadius. - Tokens and CSS — a stylesheet on the runtime's own markup for what tokens can't express, such as button weight, pill choices, the header or the card border, scoped to the theme.
- Components — only where the look needs markup or behavior the runtime's doesn't have: a branded layout, a wrapper for an animated underline, keyboard shortcuts. Forms take such a piece only when they use it.
Started from classic, the result looks like this:
themes/acme/
├── index.ts # tokens: colors (light and dark), fonts, radius, spacing, layout
├── acme.css # depth 2: chrome details tokens can't express
├── fonts/ # optional: web fonts, e.g. from `open-form theme font`
└── THEME.md # mood, palette and every token, typography, what's CSS and what's a component, signature ideas, copy tone, do / don'tA theme with its own components uses index.tsx. Every color gets a dark value
too, or dark: false for a light-only brand; a dark-only brand sets
colorScheme: 'dark' and puts its dark palette in colors. Controls and
buttons stay at least 44px tall.
A custom Layout, or CSS that changes .of-root's padding, keeps the
top-right corner clear for the language picker translated forms show, at every
width: the agent reserves the room or moves the picker. See
Layouts and chrome.
THEME.md is what Studio shows on the theme's page and what /create-form
reads when it compares themes. See Themes and
defineTheme.
Fonts
The product's own font setup (next/font, the geist package) isn't available
to forms. The agent writes a system stack with the brand font first, and ends
every stack with var(--of-cjk) (or var(--of-cjk-serif)) before the generic
family, so Chinese and Japanese text gets fonts that match the form's
locale. When the font is central to the brand, or you ask, it downloads it
into the theme with open-form theme font:
npx open-form theme font acme Inter # → themes/acme/fonts/When theme font can't reach Google (fetch failed), the agent checks which
font hosts answer. If the CSS and font files are reachable and only the catalog
isn't, it installs the family by hand into the same files; otherwise it uses a
system stack with the same feel and gives you the exact command to run later.
For Chinese and Japanese forms it picks the CJK type as deliberately as the
Latin type, one family per language scoped with :lang(), and keeps Latin-only
styling (uppercase labels, italic help, wide letter-spacing) off CJK text.
It never links fonts.googleapis.com: that would send every respondent's IP
address to Google, a problem under the GDPR. See
Web fonts.
Checking it
npx open-form theme check acme # WCAG contrast, light and dark
npm run typecheck # tsc --noEmit: the theme's code compilesAccessibility wins over brand for text and controls. When a brand color fails,
the agent keeps the hue, lowers the lightness until it passes, and tells you the
original and the new value. See open-form theme.
Contrast numbers don't show whether it looks right, so with a browser tool the
agent also opens the theme's page in Studio (/studio/themes/<id>), takes
screenshots in light and dark, and compares them with the source. Once the
theme is in a translated form, it also checks at phone and tablet width that the
language picker in the corner covers nothing. Without a browser tool, it tells
you it hasn't seen the theme.
Applying it
The new theme is a reference, so creating it changes no form, and no form ever imports it. A new form starts from its tokens:
npx open-form create waitlist --theme acmeThen the agent writes the rest of that form's look in its own theme/ folder,
taking from the reference only the CSS and components the form uses. For a form
that already exists, it sets the tokens in the form's theme/ and keeps what the
form already has, looking at it first. "Our forms" or "everything" means doing
that for each form you mean; there's no project-wide theme. It reports the
theme's folder, where the tokens came from, any contrast adjustments, and which
forms now use it.
The folder in themes/ is yours from then on: edit it by hand, or ask the
agent. The theme's page in Studio is read-only, but its Change this theme
box turns what you describe into a prompt to paste to the agent. Those changes
reach forms you start from it afterwards, never existing ones. See
Themes.