Velloo documentation
Markdown for LLMs ↗Theme your design
Four ways to seed a theme, the design-language stage on the canvas, 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. An agent proposes a theme through the MCP operations below, and you refine it on the canvas in the design-language stage. See Themes for the model itself.
Seed a theme
Four 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 } }
A few things the import does for you, and one it tells you about:
- It follows the stylesheet’s imports. An app’s global stylesheet is often only an entry point:
@import "tailwindcss"; @import "./theme.css";. Given acssPath, the import follows relative@imports inside the app and reads the tokenstheme.cssholds.importedStylesheetslists the files it read. A package or URL import is not followed; pass that file’s own path. Pastedcsshas no location, so its imports are left alone. --radiusreplaces the corner scale.--radiusis the base therounded-*steps derive from, so a stylesheet that states it replaces the theme’s radius scale with that base, plus any--radius-<step>it names outright. The steps this drops appear inchangeswithto: null, so review a dry run before re-importing over steps you set by hand.- It traces fonts a loader sets. A font
next/fontsets at runtime is in no stylesheet. The import follows its variable to the loader call in the app’s layout and sets the role, family, and weights.fonts: [{ role, family, from }]says which, and from where. - It lists what it left behind.
notImported: [{ what, reason }]names what the app states that did not reach the theme: a font it could not trace (a local font file),theme.extendsettings the theme has no token for, a body font the app never assigns tosans, or a dark palette the stylesheet does not define. Empty means the import left nothing it saw behind, not that the theme is the app’s.
Work through notImported before composing. Name an untraced font with set_theme { fonts }. When it reports a dark palette, the stylesheet has none: colorsDark is whatever the theme already had, so say that rather than presenting it as matched.
A stylesheet with no custom properties has nothing to import, and import_theme says so. That is the normal case for an app themed in JavaScript. velloo init reads a Mantine or MUI theme itself, so compare get_theme with the app’s theme object and change only what differs.
velloo init runs this same import when it creates a design in an app, so on a fresh design the dry run mostly confirms what is already there. It is still the first move when porting an existing page; match the palette before composing anything.
From a DESIGN.md
DESIGN.md is a Google Labs format for describing a visual design system to coding agents. It combines machine-readable tokens with written design rationale, so agents can read both the values and the intent behind them.
When the app has a design-system DESIGN.md, velloo init offers to seed the theme from its tokens, layered over the app’s stylesheet so unspecified roles keep their values. --no-design-md skips this theme-import offer; the document can still supply guidance to agents.
To import it later, preview the mapping first:
{ "tool": "import_theme", "args": { "designMdPath": "DESIGN.md" } }
Review coverage, dropped, and warnings, then repeat with apply: true. designMd accepts the document text instead of a path. mode: "dark" imports into the dark palette; *-dark color twins in a document also map to dark roles. Material 3, Bootstrap-style, and canvas/ink/hairline color vocabularies map onto Velloo’s semantic slots.
The imported tokens and the written rules have different lifecycles. Tokens change when imported; Velloo reads the followed document’s current text for guidance and export. get_theme reports designSystem.path. Agents should read its typography, layout, and Do’s and Don’ts before composing and use those rules when reviewing a design. Configure an app-relative path with designSystem.path in config.json when the file has another location.
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" } } }
The design language on the canvas
A theme’s tokens are half of a design’s look. The other half is its rules in words. The canvas sets both in one place, the Design language stage: colour, type, shape, and rules beside a specimen of the design’s own components.

Open it from Open the design language in the Theme rail, or Design language in the settings menu. A design with no boards yet opens on its entry page, with two ways to start:
- Read it from my app. Copy the prompt for your coding agent. It matches the app’s theme and fonts and proposes the rules, and the page opens the proposal when it lands. This is the last step of setup.
- Define it here. Four short steps on live components, with no agent needed.
Guided first, review after
A first definition is a four-step flow: Colour, Type, Shape, Rules. Once a design has a language, the stage lays the same four out to move between freely.
- Colour. Pick one of eight starting directions or a seed colour of your own, then change any role, light and dark. A direction’s card stays lit only while the theme still matches it exactly. Add custom roles, each a named colour with a light and a dark value, for what the standard roles don’t cover. The roles are grouped by what your design’s framework reads, and a contrast report flags the pairs that fail.
- Type. Choose the faces and the rhythm. The specimen’s type ladder follows as you go.
- Shape. How round the corners are, everywhere.
- Rules. The intent in a sentence or two, and the Do’s and Don’ts.
Edits are live, and Cancel restores
Every change is an ordinary theme or rule write, so it shows at once on the boards that use the theme. Cancel puts the theme and the rules back to how they were when you opened the stage. Done keeps them. When you keep a change on a design that already has boards, the stage offers a prompt for your agent to bring those boards in line.
The stage edits the theme the board on screen uses, not always the default. The Theme rail has the same colour, type, and shape controls, for a quick change without the specimen.
The specimen
The specimen draws the theme on real components: the colour roles, the type ladder, the corner scale, your app’s own components as its call sites draw them, and a few from each shelf of the library, in light and dark. It is a view, not a board or a screen. Nothing is written to the design folder for it, and an agent cannot compose onto it.
Rules live in guidance.md
The rules are saved to guidance.md in the design folder, under two sections: Overview for the intent, and Do's and Don'ts. Your agent reads the file before composing, and a review quotes its rules.
When the repository has its own DESIGN.md, the design follows that file instead. The stage shows its rules read-only and Velloo never writes it. Edit the rules in that file; they are picked up as soon as it is saved.
How an agent proposes one
set_design_language is the agent’s half. It is the last step of proposing a design language, after the theme is in:
{
"tool": "set_design_language",
"args": {
"intent": "A calm operations tool: dense tables, quiet chrome, one accent.",
"rules": [
{ "kind": "do", "text": "One filled button per view" },
{ "kind": "dont", "text": "Shadows on cards" }
]
}
}
rules is the complete list and replaces the saved one; omit intent or rules to keep what is there. The result tells the agent to send you to the canvas to review: the proposal is not agreed until you have looked at it. In a design that follows a repo DESIGN.md, the call is refused and says so.
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" } } }
A name is kept as you type it: letters of either case, digits, spaces, -, and _. “Night Shift” is a valid name and is saved as theme/Night Shift.json. Names are unique without regard to case, and default is never available. add_theme, update_theme, and the canvas apply the same rule.
On the canvas, New theme in the Theme rail asks for a name, then takes you through the guided flow above to define it. The new theme starts as a copy of the current one and is a draft until you finish: Cancel removes it, and Done puts the board on it.
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. - HTML and other inline-style folders:
velloo-theme.csswith the theme’s CSS variables and custom CSS. Link it from the app’s layout before the app’s own stylesheets. - 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.
Export a DESIGN.md
Export the theme as a design-system document instead of framework files:
{ "tool": "emit_theme", "args": { "outputDir": "../design-review", "format": "design-md" } }
The terminal equivalent is:
velloo theme export --to ../design-review --format design-md
Preview the diffs, then add apply: true or --apply to write. This produces DESIGN.md and, when the theme has dark colors, DESIGN.dark.md. Prose comes from the current followed document when available; otherwise it is generated from the tokens. Export skips writing a file that is the document the design follows and reports a warning. Choose a separate output directory, review the result, and merge deliberately.