The Visual Website Builder
You Actually Own. React + Next.js, low-code, self-hosted. Your data, your infrastructure, every feature included.
© ChaiBuilder. All rights reserved.

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.
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.
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.
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.
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.
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.
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:
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.
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.
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.
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.
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.
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.
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.
primary and confirm buttons and links across the canvas update.<style id="theme-variables">
contains your values in both the :root and .dark blocks.dark class on <html> in devtools and confirm the
published page flips cleanly.getChaiBuilderTailwindConfig, getChaiBuilderTheme