ChaiBuilder Logo

Theming

The theme is the site's design foundation: one palette, two fonts, one corner radius, shared by every page. It is edited from the Theme panel in the builder, stored with the site, and emitted as CSS variables that both the builder canvas and the published page read.

Changing the theme recolors everything at once. That is the point - blocks are styled with semantic classes like bg-primary and text-muted-foreground rather than bg-blue-600, so the theme stays the single place a brand change happens.

What the theme contains

Exactly three groups:

Group What it holds
Fonts A heading family and a body family, picked from registered fonts
Border radius One radius value in pixels, from which large/medium/small are derived
Colors 19 semantic colors, each with a light value and a dark value

There is no spacing scale, shadow scale, or type scale in the theme. Those stay in the utility classes on each block, or in a design token if you want them reusable.

Colors

Every color is a pair - a light-mode value and a dark-mode value - stored as 6-digit hex. The panel groups them the way you tend to change them:

Panel group Keys
Body background, foreground
Primary primary, primary-foreground
Secondary secondary, secondary-foreground
Border, Input & Ring border, input, ring
Card card, card-foreground
Popover popover, popover-foreground
Muted muted, muted-foreground
Accent accent, accent-foreground
Destructive destructive, destructive-foreground

The -foreground half of a pair is what sits on top of the other half. primary is the button fill, primary-foreground is the label on it. Keeping that relationship correct is what makes a theme swap safe: change primary to a dark navy and primary-foreground to white and every button, badge, and link that uses the pair stays readable.

background/foreground are the page defaults, muted is for secondary surfaces and disabled states, accent is the hover/highlight surface, border/input/ring cover outlines and focus rings.

Which half of each pair you are editing follows the dark-mode toggle in the Colors header. Toggle to dark, edit a swatch, and you have changed the dark value only.

Border radius

One number, in pixels, between 0 and 50. From it the system derives three steps:

Class Value
rounded-lg the radius you set
rounded-md radius − 2px
rounded-sm radius − 4px

Set 0 for a hard-edged brand, 6 for the default, 16 for something soft. Values under 4px make the derived small step compute below zero, which browsers clamp to square - fine in practice, but it means 0, 1, 2, and 3 look nearly identical on small elements.

Fonts

The theme stores two family names, heading and body. The dropdowns list every font registered in the project, and the choice drives --font-heading and --font-body, the font-heading and font-body classes, and the base rules that give h1-h6 the heading family and body the body family.

Adding a family to those dropdowns is a code change. See Custom fonts.

Presets

The Presets dropdown ships five complete themes:

Shadcn default, Twitter theme, Solarized theme, Claude theme, Supabase theme

Select one and click Apply. It replaces colors, fonts, and radius together. A toast appears with an Undo action for 15 seconds - use it immediately if the preset was a mistake, because it is the only one-click way back to the previous theme.

Presets are a good starting point rather than a destination. Apply the one closest to your brand, then adjust primary and the fonts.

A project can ship its own preset list in code instead, through the themePresets builder prop. Supplying presets replaces the built-in five rather than adding to them, which is what you want for a locked-down brand.

Importing a theme from CSS

If you already have a shadcn-style stylesheet - a :root block and a .dark block of CSS variables - you can paste it in rather than clicking through 19 swatches. The importer reads the color variables, --radius, and the font variable, and maps them onto the theme.

Two things to check after an import:

  • Fonts collapse to one. The importer reads a single font variable and assigns it to both heading and body. Set them separately afterwards if they differ.
  • Missing variables stay at their current value. Only four colors are strictly validated, so a partial stylesheet imports without complaint and leaves gaps. Scan the swatches once before publishing.

Alpha channels are dropped - an 8-digit hex is truncated to its 6-digit RGB. Use opacity modifiers in classes (bg-primary/90) instead of baking transparency into the theme.

Generating a theme with AI

When the AI features are enabled, the Theme panel gets a generator. Describe the look you want - "warm editorial, cream background, deep green accents" - and it produces a full theme: all 19 colors in both modes, a radius, and fonts chosen from your registered families only.

The result lands in the panel like a preset, with the same 15-second Undo. It draws on the site's brand look and tone if those are set. Nothing is published until you publish.

See AI overview and AI setup.

Dark mode

The theme always carries dark values, but showing them on the live site is your decision.

In the builder, a dark-mode switch in the canvas top bar and in the Colors header previews the dark palette and tells the color pickers which half of each pair to edit. It is a local preview toggle - it is not a site setting, and it is not published. The switch is behind the darkMode feature flag, which is off by default.

On the published site, ChaiBuilder emits the dark palette but never activates it. Dark mode turns on when the dark class is present on the <html> element, and your application owns that - whether from a user toggle, a stored preference, or prefers-color-scheme. A site that never adds the class simply always renders light, with the dark values sitting unused.

What reaches the page

The theme is emitted as a <style id="theme-variables"> block containing a :root rule with the light values and a .dark rule with the dark values:

:root {
  --font-heading: "Inter", ui-sans-serif, system-ui, sans-serif;
  --font-body: "Inter", ui-sans-serif, system-ui, sans-serif;
  --radius: 6px;
  --background: 0 0% 100%;
  --foreground: 0 0% 9%;
  --primary: 220 90% 53%;
  /* ...one line per color key */
}
.dark {
  --background: 0 0% 14%;
  /* ...the dark half of each pair */
}

Colors are stored as bare HSL triplets, not hsl(...) strings, so utilities can wrap them as hsl(var(--primary)) and still support opacity modifiers like bg-primary/90.

The classes this gives block authors:

Kind Classes
Backgrounds bg-background, bg-card, bg-popover, bg-muted, bg-accent, bg-primary, bg-secondary, bg-destructive
Text text-foreground, text-muted-foreground, text-card-foreground, text-primary-foreground, text-secondary-foreground, text-accent-foreground, text-destructive-foreground
Borders and rings border-border, border-input, ring-ring
Radius rounded-lg, rounded-md, rounded-sm
Fonts font-heading, font-body

Every theme color also works with the other color-consuming prefixes - from-, via-, to-, divide-, outline-, shadow- - and the class autocomplete in the styling panel offers all of them.

Alongside the theme variables, the page emits <style id="fonts-styles"> for the @font-face rules and <style id="page-styles"> for the compiled page CSS. Layout routes emit the same CSS through hoisted styles instead of ids.

Tailwind setup

ChaiBuilder supports both Tailwind v3 and v4, and the theme reaches utility classes differently in each.

Tailwind v4 is the default. The theme is bridged with a generated @theme static block that maps each variable into Tailwind's namespace - --color-primary: hsl(var(--primary)), --radius-lg: var(--radius), and so on. No JS config is involved.

Tailwind v3 uses a JS config. Extend yours with the ChaiBuilder preset so your own components resolve the same classes as builder blocks:

// tailwind.config.js
import { getChaiBuilderTailwindConfig } from 'chaipro/utils'

export default getChaiBuilderTailwindConfig({
  content: ['./src/**/*.{ts,tsx}'],
})

That sets darkMode: 'class', maps every theme variable to a utility, and loads the typography, forms, and container-query plugins. getChaiBuilderTheme() is also exported if you only want the theme.extend fragment to merge into an existing config.

Storage and publishing

The theme is stored as a single JSON object on the site record - one theme per site, not per page. Edits save automatically, debounced by about a second.

Like design tokens, a theme edit is a draft change. The builder and draft preview show it immediately; the live site keeps the previously published theme until you publish. It appears in the publish list as Theme.

Editing the theme is permission gated. A role without theme edit access sees the panel replaced with a message rather than the controls.

Gotchas

Six-digit hex only. The color picker accepts #1155FF, not #15F, not hsl(...), not oklch(...). Shorthand hex from a brand guide needs expanding first.

The palette keys are fixed. You cannot add a brand-tertiary color through the Theme panel. If a project needs colors beyond the 19, put them in utility classes or a design token rather than expecting a new swatch.

A few color names exist in the class list but have no theme value. bg-surface, bg-success, bg-warning, bg-info and similar compile to an unresolved variable and render as nothing. Stick to the keys the Theme panel actually shows.

Preset Undo does not survive a reload. The 15-second toast is the window. Once it is gone, or the tab is refreshed, restoring the old theme means re-entering the values - another reason to export or note a palette you care about before experimenting.

Radius is silently clamped. Anything outside 0-50, or non-numeric, becomes 0 with no message.

Dark values are easy to forget. Editing swatches with the dark toggle off changes only the light half. If your site ships dark mode, walk the palette a second time with the toggle on before publishing.

Verify

  1. In the builder, change primary and confirm buttons and links across the canvas update.
  2. Toggle dark mode in the canvas and check text stays readable on every surface.
  3. Publish, then on the live page view source and confirm <style id="theme-variables"> contains your values in both the :root and .dark blocks.
  4. If your site ships dark mode, add the dark class on <html> in devtools and confirm the published page flips cleanly.
  • Design tokens - reusable class bundles built on these colors
  • Custom fonts - registering families for the font dropdowns
  • Blocks - where theme classes are applied
  • Configuration - feature flags including dark mode and theme import
  • API reference - getChaiBuilderTailwindConfig, getChaiBuilderTheme

© ChaiBuilder. All rights reserved.