Velloo documentation
Markdown for LLMs ↗Theme your design
Three ways to seed a theme, token-level fine-tuning, contrast gates, named themes per board, and emitting the result into your app.
Velloo’s theme is a single token tree (colors, typography, spacing, radius, shadows) stored as JSON in the design folder and projected natively per framework: CSS variables and Tailwind utilities for shadcn, a MUI theme for MUI. The agent is the primary theme author; every operation below is an MCP tool. See Themes for the model itself.
Seed a theme
Three entry points, depending on where your taste is coming from.
From an existing app
If you’re designing against an app that already has a look, import it. import_theme parses your stylesheet (cssPath is absolute or relative to the host app root, since globals.css lives outside the design folder). It reads shadcn-convention :root/.dark custom properties, Tailwind v4 @theme color vars, --radius, and --font-* roles, and, given a cssPath, also ingests the nearby tailwind.config theme.extend (brand colors, named spacing, shadows, font families, keyframes).
It dry-runs by default and returns the change list:
{ "tool": "import_theme", "args": { "cssPath": "app/globals.css" } }
Review the changes: [{ token, from, to }] and the coverage summary (zero mapped semantic slots means the app’s variables landed only in palette.* and you still need to map colors.* with set_theme), then persist:
{ "tool": "import_theme", "args": { "cssPath": "app/globals.css", "apply": true } }
This is the first move when porting an existing page; match the palette before composing anything.
From a seed color
One brand color in, a full OKLCH-based palette out, with foreground/background pairs nudged toward WCAG AA contrast. Seeding is the from channel of set_theme, the one verb for every theme edit:
{ "tool": "set_theme", "args": { "from": { "seedColor": "#e85d2f" } } }
Accepts #hex, oklch(), rgb(), hsl(), named colors, anything CSS parses as a color.
From a preset
Twelve curated presets ship in the binary: default-light, default-dark, violet, emerald, amber, rose, indigo, ocean, slate, forest, sunset, plum. Each is a complete token tree, so applying one swaps the palette wholesale.
{ "tool": "set_theme", "args": { "from": { "preset": "ocean" } } }
Fine-tune with set_theme
set_theme takes any combination of five channels in one call (from, tokens, fonts, typeset, and customCss), and at least one is required. from runs first, so tokens in the same call override the freshly seeded palette rather than the old one. Pass theme to edit a named theme; omitted, it edits the folder default.
Tokens
tokens patches dot-paths. The full theme is schema-validated after each patch:
{
"tool": "set_theme",
"args": {
"tokens": {
"colors.primary.DEFAULT": "oklch(0.62 0.19 41)",
"palette.ink": "#16130f"
}
}
}
The colors.* slots (and their colorsDark.* twins) are the semantic, dark-flipping roles (background, foreground, primary, muted, and so on). The palette.* namespace is a raw passthrough that does not flip: palette.ink makes bg-ink / text-ink / border-ink resolve literally on the canvas, useful for brand colors imported from an app. A palette token named after a semantic slot (palette.muted) is skipped on emit and comes back with a warning; set the slot itself or rename the token.
Fonts
Each font role becomes a --font-<role> variable and a font-<role> utility class. Google-flagged families load in design mode and emit an @import into the generated globals.css; google is true for a plain load or an axis spec such as "wght@400..900":
{
"tool": "set_theme",
"args": {
"fonts": [
{ "role": "display", "family": "Fraunces", "google": true },
{ "role": "sans", "family": "Inter", "google": true }
]
}
}
Declare a display face before composing, not after; big type is the cheapest personality lever a design has. { "role": "display", "remove": true } drops a role, and is refused while a typeset still names it.
Typesets
A typeset is the typographic rhythm the whole type ladder derives from: size (the base text size, "1em" or 15), leading (unitless body line-height; heading leading derives from it), flow (the space between blocks), and which font roles paint body, headings, and code (fontBody, fontHeading, fontMono). They live under typography.typesets in the token tree, and get_theme reports what they compute to as typeScale.
{
"tool": "set_theme",
"args": {
"typeset": [
{ "size": 16, "leading": 1.6, "flow": "1.25em", "fontHeading": "display", "fontBody": "sans" },
{ "name": "docs", "size": 15, "leading": 1.7 }
]
}
}
An entry without a name edits the default typeset, the folder baseline behind Heading levels and Text variants. Any other name becomes a preset that a Prose region opts into with preset: "docs", and projects as a .typeset-docs class. Entries also take renameTo and remove. Set the typeset once, early: one call re-proportions every screen, which beats overriding text-* node by node.
The escape hatch
customCss replaces theme/custom.css wholesale: keyframes, grain textures, ::selection, anything tokens don’t express. It’s folder-wide, injected into every render, and appended to the emitted globals.css. Read the current contents first from get_theme’s customCss field, since the write is a full replacement:
{ "tool": "set_theme", "args": { "customCss": "@keyframes drift { from { transform: translateY(0) } to { transform: translateY(-8px) } }" } }
Gate on contrast
After reseeding from a color or a preset, score the theme before shipping it:
{ "tool": "score_theme_contrast", "args": {} }
Returns WCAG contrast ratios for the theme’s salient color pairs (foreground/background, primary/primary-foreground, and the rest) in both light and dark palettes, each tiered AAA, AA, AAlarge, or Fail. Pass mode to score one palette, or theme to score a named theme. A Fail on a body-text pair is worth fixing before anyone screenshots anything.
Named themes and board pinning
A folder can hold more than one theme. add_theme clones the default theme (or the one named in from) to theme/<name>.json; boards pin one so different flows render under different looks:
{ "tool": "add_theme", "args": { "name": "midnight" } }
{ "tool": "update_board", "args": { "boardId": "marketing", "patch": { "theme": "midnight" } } }
list_themes enumerates the named themes and which boards use each; update_theme renames one (repointing its boards) and remove_theme deletes one no board pins. Edit a named theme’s tokens with set_theme { theme: "midnight", … }. get_theme, screenshot, compare_to_url, score_theme_contrast, and emit_theme also take theme, so you can read, render, score, or emit a named theme without pinning it anywhere.
Emit the theme
The theme emits as the target framework’s native artifact (a direct, user-facing file, unlike emit_code’s agent-consumed IR). Paths are relative to outputDir:
- shadcn / Tailwind v4:
app/globals.css(override withcssPath), a siblingtypeset.cssthat globals.css@imports, and atailwind.config.ts(passcssOnly: trueto skip it).theme/custom.cssis appended to globals.css. - shadcn / Tailwind v3: detected from
outputDir’s package.json, or forced withtailwind: 3(tailwind: 4forces the other way). Emitsvelloo-theme.css(variables as HSL triplets),velloo-typeset.css, and avelloo.preset.tsorvelloo.preset.cjs(skipped withcssOnly), plusnoteswith the one-time@importandpresets: [...]wiring. - MUI: a
createTheme(...)module atthemePath(defaulttheme.ts), with adarkThemeexport when the theme has a dark tree. Chakra emitsextendTheme(...)tochakra-theme.ts; Ant Design emitsantd-theme.ts. - Every framework: a framework-neutral DTCG (Design Tokens Community Group)
tokens.json.
Dry-run first; it returns each file’s contents and a diff against what’s on disk:
{ "tool": "emit_theme", "args": { "outputDir": "../apps/web" } }
Then write:
{ "tool": "emit_theme", "args": { "outputDir": "../apps/web", "apply": true } }
The CLI equivalent
velloo theme export is the same path from the terminal. It runs in diff mode by default and never auto-overwrites your files:
velloo theme export --to ../apps/web # print diffs
velloo theme export --to ../apps/web --apply # write the files
In a TTY it shows the diffs and asks before applying; without a TTY, re-run with --apply. It routes by framework exactly like emit_theme, native theme modules and tokens.json included. On a Tailwind folder it detects a Tailwind v3 target app and emits the v3 projection (velloo-theme.css, velloo-typeset.css, velloo.preset) with the wiring notes; --force-v4 emits the v4 artifacts instead. --css-only skips the config or preset file, and --theme points at a different theme JSON file than theme/default.json.