Open Form

defineTheme

Theme tokens, dark mode, components and the --of-* CSS variables.

A theme's index.ts — index.tsx when it adds components — default-exports defineTheme({...}). It lives in one of two places:

  • forms/<id>/theme/ — the form's own copy, made with open-form theme copy or open-form create <id> --theme <theme>. The form imports it from ./theme/index.ts, or index.tsx when the theme adds components (theme copy prints the import), and passes it to <Form theme>. Only that form uses it.
  • themes/<id>/ — a reference for new forms, with a THEME.md beside it that describes the look for people and agents. No form uses it directly, so editing it never changes a form's copy.
forms/meetup/theme/index.tsx
import { defineTheme, SplitLayout } from '@open-form-build/core';
import './brand.css';

export default defineTheme({
  font: '"Inter", system-ui, var(--of-cjk), sans-serif',
  headingFont: 'Georgia, var(--of-cjk-serif), serif',
  radius: '12px',
  buttonRadius: '999px',
  spacing: 'comfortable',
  width: '640px',
  colors: {
    primary: '#0f766e', primaryForeground: '#ffffff',
    background: '#f5f7f6', surface: '#ffffff',
    foreground: '#0b1f1c', muted: '#5b6b68',
    border: '#dbe3e1', control: '#7d8f8b', input: '#ffffff',
    error: '#c0362c', success: '#15803d',
  },
  dark: {
    primary: '#5eead4', primaryForeground: '#042f2e',
    background: '#071311', surface: '#0d1d1a',
    foreground: '#ecf5f3', muted: '#9bb0ab',
    border: '#1f3330', control: '#5d746f', input: '#0a1816',
    error: '#f97066', success: '#4ade80',
  },
  components: { Layout: SplitLayout },
});

Tokens

Prop

Type

Colors

Every color is any CSS color — hex, rgb(), hsl(), oklch(). Keep them as literal values: open-form theme check can't resolve var() or color-mix(), and Studio's Adjust panel edits literals in place.

KeyCSS variableUsed for
primary--of-primaryButtons, selection, focus rings
primaryForeground--of-primary-fgText on primary
background--of-bgThe page behind the card
surface--of-surfaceThe card
foreground--of-fgText
muted--of-mutedHelp text and secondary copy
border--of-borderDecorative edges: card, dividers, choice tiles
control--of-controlOutlines that identify inputs, checkboxes, radios, ratings
input--of-inputBackground of inputs, selects and choice tiles
error--of-errorError text and outlines
success--of-successSuccess states

Dark mode

  • Without dark, the colors you set apply in dark mode too, and any color you leave out takes the runtime's dark default, so your light colors mix with built-in dark ones. open-form theme check warns about this. Always give dark, or dark: false for a light-only theme.
  • colorScheme: 'dark' makes a dark-only brand: put the dark palette in colors.

Contrast

pnpm exec open-form theme check <id> measures every pair below in light and dark (WCAG); add --form to check a form's own copy, <id> being the form id. Run it after any color change.

PairMinimum
foreground on surface, background and input4.5:1
muted on surface4.5:1
primaryForeground on primary4.5:1
error on surface4.5:1
control on input3:1
primary on surface3:1

border is decorative and exempt, which is why inputs are outlined with control: keep control at 3:1 or more against input so people can find the fields.

CSS variables

Tokens become CSS variables on the form root. Use them in theme CSS, form CSS and custom fields so every theme and dark mode keeps working.

VariableDefault
--of-font, --of-heading-fontSystem sans-serif
--of-radius10px
--of-button-radiusvar(--of-radius)
--of-gap24px
--of-width640px
--of-aside-width340px
--of-primary … --of-successThe built-in palette, the same as the classic starter's
--of-ringprimary at 24%, for soft focus halos
--of-hover-border, --of-hover-controlHover shades of border and control
--of-easeThe runtime's easing curve
--of-cjk, --of-cjk-serifCJK fallback fonts matching the form's language

Han characters are drawn differently in Taiwan, mainland China and Japan, so --of-cjk follows the form's language. Put it after your Latin fonts instead of naming one region's font. To use a web font, see Web fonts.

Theme CSS

Import a stylesheet from the theme's index file for details tokens can't express — button weight, header treatment, card border. Prefix theme classes, and scope overrides of built-in chrome (.of-form, .of-title, .of-button) with [data-of-theme="<id>"]:

themes/brand/brand.css
[data-of-theme="brand"] .of-button-primary {
  font-weight: 600;
  letter-spacing: 0.01em;
}

A form's root carries data-of-theme="<form-id>", so in a form's copy the scope is the form id. open-form theme copy brand meetup rewrites the selector above to [data-of-theme="meetup"]. If you rename the form's folder, rename the selectors too.

Prefixed classes such as .brand-tile aren't scoped. That only matters when several forms share a page and their copies of one theme have diverged; give one of them a new prefix.

A form's theme is bundled with the form, on the client and in the server bundle (without its CSS), so don't touch window or document at the top level of theme files.

On this page