Skip to content

Creating Themes

Baan’s theme system is fully open — every built-in theme is a folder of React components and CSS variables that you can inspect, fork, and modify. New themes can be created in three ways: describing what you want in the admin panel, using Claude Code with the /create-theme skill, or building one from scratch following the theme specification.

Every Baan theme also ships with its design prompts (see Theme Provenance). This is a core part of the theme specification — unlike traditional theme systems that distribute only the compiled code, Baan themes carry the intent and AI agent context that produced them.

Method 1: AI Theme Generation (Admin Panel) Beta

Section titled “Method 1: AI Theme Generation (Admin Panel) Beta”
  1. Go to Admin → Settings → Theme
  2. Click Create new theme
  3. The panel opens with AI Agent (recommended) and Admin AI (beta) tabs
  4. To use Admin AI: switch to the Admin AI tab, enter a theme name and prompt, select a model, and click Generate
  5. The new theme appears in the theme list — select it and preview with the Customizer
  • “Minimalist Japanese design, lots of whitespace, serif headings, muted gray palette”
  • “Bold magazine layout, full-bleed header images, high contrast, modern sans-serif”
  • “Dark mode only, neon accent colors, cyberpunk aesthetic, monospace body text”
  • “Clean academic journal style, Chicago citation format look, classic typography”

The recommended approach for high-quality themes. Use Claude Code, Cursor, or Codex to instruct an AI agent to create the theme directly in your codebase. The agent reads Baan’s theme docs and generates a complete, working theme.

Claude Code includes a built-in /create-theme skill that runs a structured 5-step workflow. Cursor and Codex work with a natural language prompt — the Admin UI provides a copyable template on the AI Agent tab.

Open Baan’s repository in Claude Code and type:

/create-theme

Then describe what you want in the same message or when prompted:

Create a dark editorial theme inspired by longform magazines like The Atlantic.
Serif headings (Playfair Display), wide article body (75ch), minimal sidebar,
dark navy background, off-white body text, no rounded corners anywhere.

The skill follows a 5-step workflow — it doesn’t generate anything until step 4:

Step 1 — Research existing themes Claude Code reads all files under themes/ to understand what already exists: naming conventions, CSS variable patterns, which component props are used, and where the built-in themes differ from each other.

Step 2 — Differentiation check Claude Code compares your prompt to existing themes and flags if something too similar already exists. If the new theme would be redundant, it asks how to distinguish it.

Step 3 — Design proposal (requires your approval) Claude Code presents a concrete design concept before writing any code. This includes:

  • Theme name and folder name
  • Color palette (background, text, accent, muted)
  • Typography decisions (font families, scale, line height)
  • Layout structure (sidebar yes/no, nav position, content width)
  • Specific component behaviors (e.g., “post cards show only title and date, no excerpt”)

You approve, reject, or redirect before any files are created. This is the most important step — changes to the proposal cost nothing; changes to generated files cost time.

Step 4 — Implementation After approval, Claude Code generates:

themes/your-theme/
├── theme.json # Theme metadata (name, display name, version, ai field)
├── config.ts # Font imports, theme constants, exported config
├── prompts/ # Theme Provenance (required)
│ ├── concept.md # Design concept & principles (written from the approved proposal)
│ └── generation.md # Optional: the actual prompt text used for generation
├── styles/
│ └── theme.css # All CSS custom properties
└── components/
├── layout/
│ └── Layout.tsx # Root layout: header + main content area + footer
└── post/
├── PostCard.tsx # Article card used on listing pages
└── PostContent.tsx # Full article page (body, sidebar, metadata)

theme.json includes an ai field recording the generation context (see Theme Provenance). prompts/concept.md captures the design intent in English so the theme can later be forked, understood by other AI agents, or regenerated from the prompt alone.

Step 5 — Registration The theme auto-registers on the next npm run dev. The generate-theme-manifest script discovers all folders under themes/ that contain a valid theme.json and adds them to the manifest. No manual registration needed.

The quality of the output depends heavily on the specificity of the prompt. Here’s what to include:

Layout structure — where is the sidebar, how wide is the content, does the header scroll or stick?

Left sidebar with tree navigation (like Obsidian Publish), fixed on scroll.
Main content area 70ch wide, no right sidebar.

Typography — font choices, size relationships, weights

Headings: Playfair Display, 700 weight, tight letter-spacing.
Body: 18px Georgia, 1.8 line height.
Code: JetBrains Mono.

Color system — dark/light, specific values if you have them, accent color usage

Dark theme only. Background #0d1117, text #e6edf3 (GitHub Dark palette).
Single accent: #58a6ff. No warm colors.

Feel / reference sites — named references work well

Inspired by The Verge (2023): large type, aggressive color blocks,
full-bleed images, no borders between content areas.

Explicit constraints — what you don’t want

No rounded corners anywhere.
No drop shadows.
No hover animations.
Post cards: title only, no excerpt, no cover image.

Generated themes are a starting point, not a final product. The most efficient fine-tuning workflow:

1. CSS variables first

Most visual changes can be made entirely in styles/theme.css without touching any component files. Start there:

:root {
--color-bg: #0d1117;
--color-text: #e6edf3;
--color-accent: #58a6ff;
--color-muted: #8b949e;
--font-heading: 'Playfair Display', Georgia, serif;
--font-body: Georgia, 'Times New Roman', serif;
--font-mono: 'JetBrains Mono', 'Fira Code', monospace;
--spacing-content: 70ch;
--spacing-sidebar: 260px;
--radius: 0; /* No rounded corners */
}

2. Ask Claude Code to make specific changes

After reviewing the generated theme in the browser, describe what needs to change:

In the generated theme, the post card is too tall — remove the excerpt text,
keep only the title, date, and category tag.
The header is sticky but I want it to scroll away.
Remove position: sticky and the backdrop-filter.
Increase the body font size from 16px to 18px and set line-height to 1.85.

3. Fork if you want a variant

If you want the same theme with a different color palette (e.g., a light version of a dark theme), ask Claude Code to fork it:

Fork the `atlantic-dark` theme into `atlantic-light`.
Swap the background and text values, keep all layout and typography unchanged.

4. Preview while editing

With npm run dev running, theme file changes apply on save. Use Admin → Customizer to preview the theme on your actual content across desktop, tablet, and mobile breakpoints.


For full control, create a theme folder directly in themes/[theme-name]/.

themes/my-theme/
├── theme.json # Theme metadata (includes `ai` field)
├── config.ts # Font imports, exported theme config
├── prompts/ # Theme Provenance (required)
│ └── concept.md # Design concept & principles (English)
├── styles/
│ └── theme.css # CSS custom properties
└── components/
├── layout/
│ └── Layout.tsx # Root layout wrapper
└── post/
├── PostCard.tsx # Article card for listing pages
└── PostContent.tsx # Full article page layout
{
"name": "my-theme",
"displayName": "My Theme",
"description": "A clean, minimal theme with serif headings.",
"author": "Your Name",
"version": "1.0.0",
"ai": {
"agent": "Manual",
"skills": [],
"generatedAt": "2026-04-18"
}
}

The ai field records the theme’s provenance. For hand-built themes, set agent to "Manual" and leave skills as an empty array.

Themes use CSS custom properties for all design tokens:

:root {
--color-bg: #ffffff;
--color-text: #111111;
--color-accent: #0070f3;
--color-muted: #6b7280;
--font-heading: 'Georgia', serif;
--font-body: 'Inter', sans-serif;
--font-mono: 'JetBrains Mono', monospace;
--spacing-content: 45rem;
--radius: 0.375rem;
}

Each component receives props from Baan’s data layer. Components are React/TSX files that can use Tailwind CSS or the CSS variables defined in styles/theme.css.

For the full component API and prop types, see the AI Agent Built Themes Guide in the main repository.


Themes are discovered and registered at build time, not runtime:

npm run dev / npm run build
└── generate-theme-manifest (prebuild script)
└── scans themes/*/theme.json
└── validates required files exist
└── writes lib/generated/theme-manifest.json
└── writes lib/generated/theme-components.ts
└── writes lib/generated/theme-fonts.ts
└── imported by the theme loader at runtime

The generated manifest lists all available theme IDs with their metadata. The active theme ID is stored in the settings table (key: theme). On each request, Baan’s rendering layer reads the manifest and uses the generated static component loaders to resolve the active theme.

Request → Next.js App Router
└── reads active theme ID from settings (cached)
└── loads themes/[active]/components/layout/Layout.tsx
└── Layout renders:
├── Header (from theme or Baan default)
├── <slot /> — page-specific content
│ ├── Listing page: PostCard[] from theme
│ └── Post page: PostContent from theme
└── Footer (from theme or Baan default)

Each page type passes a typed props object to the theme components. The theme components are responsible only for rendering — all data fetching happens in Baan’s page layer before the theme components are called.

Themes define variables on :root. Baan’s global stylesheet defines fallback values for every variable so that a partially specified theme still renders correctly:

Baan global defaults (lowest priority)
└── theme :root variables (override)
└── component-scoped overrides (highest priority)

This means you can create a minimal theme that only overrides a few variables and inherits the rest from Baan’s defaults. The built-in geek theme, for example, primarily overrides font and spacing variables while inheriting the color system from default.

Theme components run inside a React error boundary. If a theme component throws an error (e.g., accessing a prop that doesn’t exist in a particular context), Baan renders a fallback UI rather than crashing the page. This means a broken theme never takes down the entire site.

Theme components are loaded with next/dynamic (lazy loading) so they don’t affect the bundle size of other themes.

Layout.tsx can import and render custom Header and Footer components from within the theme folder. If a theme doesn’t define its own header or footer, Baan’s default nav renders automatically.

To add a theme-specific header:

themes/my-theme/components/layout/Layout.tsx
import Header from './Header'
import Footer from './Footer'
export default function Layout({ children, site }) {
return (
<>
<Header site={site} />
<main>{children}</main>
<Footer site={site} />
</>
)
}
VariableDefaultPurpose
--color-bg#ffffffPage background
--color-text#111111Body text
--color-accent#0070f3Links, buttons, highlights
--color-muted#6b7280Secondary text, dates, tags
--color-border#e5e7ebDividers, card borders
--font-headingsystem-ui, sans-serifHeading font stack
--font-bodysystem-ui, sans-serifBody text font stack
--font-monomonospaceCode blocks
--spacing-content45remMax width of article body
--spacing-sidebar280pxSidebar width
--radius0.375remBorder radius for cards/buttons

After placing the theme folder in themes/, restart the dev server (or run npm run build in production) to register the new theme. It will then appear in Admin → Customizer and can be activated from there.


Baan specifies Theme Provenance: every theme ships with the prompts and AI-agent context that produced it, not just the compiled code. This is a core part of Baan’s theme specification and sets it apart from traditional theme systems like WordPress, Hugo, or Astro.

themes/<name>/
├── theme.json # includes "ai" field
└── prompts/ # required
├── concept.md # required — design concept & principles (English)
├── generation.md # optional — the actual generation prompt
└── skills/ # optional — custom Claude Code skill files bundled with the theme
└── <name>.md
{
"ai": {
"agent": "Claude Code",
"skills": ["create-theme"],
"generatedAt": "2026-04-18"
}
}
FieldPurpose
agentWhich AI agent produced the theme: "Claude Code", "Cursor", "Codex", "Baan Admin", "Manual", etc.
skillsOptional array — built-in skill names (e.g. "create-theme") or references to files in prompts/skills/. Both forms can coexist in the same array.
generatedAtOptional ISO date.

The ai field is optional at the schema level — missing ai is treated as “unknown provenance” (no error). The theme-manifest generator does not read prompts/; build output is unaffected by provenance data.

# <Theme Name> — Theme Concept
## Overview
<1-2 sentence positioning statement>
## Design Principles
- ...
## Target Users
<who this theme is for>
## Layout
<high-level layout description>
## Typography
<font choice + rationale>
## Color Palette
<light/dark approach, key CSS variables>
## Component Notes
<any theme-specific component behavior worth documenting>
  • Fork through the prompt, not the code — edit concept.md and hand it to Claude Code or Cursor to regenerate a variation, even as a non-engineer.
  • Carry intent across AI agents — the same concept document can be fed to any AI agent (results will drift — that’s expected; it’s a recipe, not a reproducibility guarantee).
  • Credit the author’s design decisions — the rationale lives inside the theme folder, not just in a separate README that can get lost.

If a theme was generated using a custom Claude Code skill (one you wrote yourself, not Baan’s built-in create-theme), bundle the skill file with the theme:

themes/my-theme/prompts/skills/my-custom-generator.md

Add the skill name to theme.json:

{
"ai": {
"agent": "Claude Code",
"skills": ["create-theme", "my-custom-generator"],
"generatedAt": "2026-04-18"
}
}

Consumers who install the theme can copy the bundled skills into their own .claude/skills/ directory using the import-theme-skills skill, which is shipped with Baan.

Themes without prompts/ still load — the system issues a warning only. External themes written before this specification existed continue to work. New themes should include prompts/concept.md going forward.