Open Form

defineTheme

Every defineTheme option: color tokens, dark mode, fonts, radius and spacing, theme components, contrast checks 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 look, written by open-form create (from a reference's tokens with --theme) and then by you or your agent. The form imports it from ./theme/index.ts, or index.tsx once it adds components, 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 imports it, so editing it never changes a form.
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

open-form theme check <id> measures every pair below in light and dark (WCAG); add --form to check a form's own look, <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. Scope everything with [data-of-theme="<id>"]: a form's root carries data-of-theme="<form-id>", so in a form's own theme the scope is the form id, and in a reference it's the theme id. If you rename a form's folder, rename the selectors too.

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

Most of a look can be CSS on the runtime's own markup, before any component:

PartClasses
Root, card, header.of-root, .of-form, .of-header, .of-title, .of-description
Steps.of-progress, .of-progress-label, .of-progress-track, .of-progress-bar, .of-page-title, .of-page-description
Field.of-field (data-invalid when it has an error), .of-label, .of-required, .of-help, .of-error
Inputs.of-input, .of-textarea, .of-counter, .of-input-group, .of-unit, .of-select-wrap
Choices.of-choices (data-variant), .of-choice, .of-radio, .of-checkbox, .of-choice-single, .of-choice-text
Rating.of-rating, .of-rating-item, .of-rating-labels
Buttons.of-nav, .of-button, .of-button-primary, .of-button-secondary
Endings.of-success, .of-success-icon, .of-notice, .of-eyebrow

Glyphs belong in pseudo-elements with empty alt text, so screen readers skip them: .of-label::before { content: "? " / ""; }. Reach for a component only when the look needs markup or behavior CSS can't give.

Prefixed classes such as .brand-tile aren't scoped. That only matters when several forms share a page and style the same class differently; 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