Skip to content

Velloo documentation

Markdown for LLMs ↗

MCP: theme

set_theme (tokens, presets, seed colors, fonts, typesets, custom CSS), named themes, import_theme, and contrast scoring.

Theme operations mutate the folder’s token tree (theme/default.json) or named themes alongside it (theme/<name>.json). On the default guided surface, invoke them through call_velloo and retrieve an exact contract with operation_schema; on the full surface they are advertised as native tools. Read the current tree with get_theme, which takes an optional theme name and also returns the computed typeScale and the folder’s customCss. For the workflow, see the theme guide; for the token model, Themes.

set_theme

Edit a theme through one verb. Pass any combination of the five channels (from, tokens, fonts, typeset, customCss) in a single call; at least one is required (a call with only theme is a BadRequest).

ArgTypeRequiredDescription
themestringnoNamed theme to edit; default "default". Create a named theme with add_theme first
fromobjectnoReseed the palette: { preset } or { seedColor }, exactly one key
tokensobjectnoDot-path patch: { "<path>": <string or number>, ... }
fontsarraynoFont roles to declare or drop; at least one entry
typesetarraynoTypesets to declare, retune, rename, or remove; at least one entry
customCssstringnoReplaces theme/custom.css entirely

Channels apply in a fixed order (from, then tokens, fonts, typeset, customCss), so a token patch in the same call overrides the freshly seeded palette, and a typeset can name a font role declared by fonts in the same call. Each channel is written as it runs: if a later channel fails, the error is returned but the earlier channels in that call have already been saved.

Returns { theme, applied }, where applied has one key per channel that ran: from (echoed), tokens (the applied paths), warnings (when present), fonts (the roles), typeset (the typeset names), and customCss: { bytes }. Call get_theme when you need the full tree.

{
  "from": { "seedColor": "#0f766e" },
  "tokens": { "radius.md": "0.75rem", "palette.ink": "#1a1a1a" },
  "fonts": [
    { "role": "display", "family": "Fraunces", "fallback": "ui-serif, Georgia, serif", "google": "opsz,wght@9..144,300..900" }
  ],
  "typeset": [{ "fontHeading": "display", "leading": 1.7 }]
}

from

  • { "preset": "<name>" } replaces the whole theme with a shipped preset: colors, typography (font roles and typesets), spacing, and radius. Anything else in the theme, including colorsDark, palette, and custom font roles, is dropped. An unknown name fails schema validation with the valid options.
  • { "seedColor": "<color>" } generates a full OKLCH palette from one color and replaces colors and colorsDark, keeping the rest of the theme. It accepts #hex, named colors, rgb(), hsl(), oklch(), and other CSS color forms; an unparseable color returns InvalidColor. Foregrounds are nudged toward black or white until each pair reaches WCAG AA (4.5:1). Follow up with score_theme_contrast to confirm.

Shipped presets: default-light, default-dark, violet, emerald, amber, rose, indigo, ocean, slate, forest, sunset, plum. Presets define colors only (no colorsDark), all sharing the same Inter-based typography.

tokens

Keys are dot-paths into the theme ("colors.primary.DEFAULT", "colorsDark.background", "radius.md", "spacing.18"); values are strings or numbers. Three color namespaces:

  • colors.* / colorsDark.*: the semantic slots (background, foreground, primary, secondary, muted, accent, destructive, card, popover, border, input, ring) that drive component chrome. colorsDark holds only the slots that differ in dark mode; the rest fall back to colors.
  • palette.* / paletteDark.*: a flat passthrough of raw brand colors that does not flip. Set palette.ink to "#1a1a1a" and bg-ink / text-ink / border-ink resolve literally, the mechanism that makes a host app’s verbatim brand classes work. Keys must be lowercase kebab (ink, primary-600). import_theme populates it automatically. A palette key named after a semantic slot (palette.primary) is skipped on emit and in the canvas; the result carries a warnings entry telling you to set colors.<slot> or rename the key.

The patch is all-or-nothing. Every entry is validated against the theme schema, and a path the schema doesn’t define (a misspelled slot, or a retired key like typography.fontSize) counts as a failure rather than being silently dropped. If any entry fails, nothing is written and the error is BulkTokensInvalid { applied, failed: [{ path, reason }] }, where applied lists the entries that would have landed.

fonts

Each entry declares a font role. A role becomes a --font-<role> token and a font-<role> utility.

FieldTypeDescription
rolestringLowercase letters, digits, and dashes: "display", "sans", "mono", or any utility-safe name
familystringFont family, e.g. "Unbounded". Required unless remove
fallbackstringCSS stack tail. Defaults by role: serif and mono get serif/monospace stacks, everything else ui-sans-serif, system-ui, sans-serif
googlestring or trueLoad the family from Google Fonts in design mode and emit an @import in the emitted globals.css: an axis spec like "wght@400..900", or true for a plain load. Omit for local or system fonts
removebooleanDrop the role. Refused when the role doesn’t exist, or while a typeset still names it

Re-declaring a family replaces its Google Fonts axis spec, and webfont loads that no role uses any more are pruned.

typeset

A typeset is the type rhythm the whole ladder derives from: three controls plus the font roles for body, headings, and code. The heading ladder (h1–h6), body, lead, small, and caption sizes, leading, tracking, and heading margins are all computed from these, so reach for a typeset instead of per-node text-* sizes.

Typesets live at typography.typesets.<name>. default is the folder baseline: it projects onto :root and styles every screen. Any other name becomes a .typeset-<name> preset class that a Prose region opts into with preset: "<name>". The shipped presets carry default, docs, chat, compact, and reading.

FieldTypeDescription
namestringTypeset to edit; default "default". Letters, digits, dash, underscore
sizestring, number, or nullBase text size: "1em" follows the container, 15 or "15px" pins it
leadingnumber or nullBody line-height, unitless and positive; heading leading derives from it
flowstring, number, or nullSpace between blocks, e.g. "1.25em"; heading margins derive from it
fontBodystring or nullA declared font role
fontHeadingstring or nullA declared font role
fontMonostring or nullA declared font role
renameTostringRename this typeset, carrying its controls over
removebooleanDelete this typeset; regions still using its class fall back to the baseline

Fields merge into the existing typeset, so tuning one control doesn’t restate the others; null clears a field back to the baseline. A name that doesn’t exist yet is created. Refusals: renameTo with remove, renaming or removing default, renaming or removing a typeset that doesn’t exist, renaming onto an existing name, a face that isn’t a declared font role, and a role named like a scale entry (h1, body, caption, …). All come back as InvalidThemePath with the reason.

{ "typeset": [{ "name": "docs", "size": 15, "flow": "1.5em" }, { "name": "chat", "remove": true }] }

typography itself holds only fontFamily, googleFonts, and typesets. The old fontSize, fontWeight, lineHeight, and letterSpacing records were retired; writing them fails validation.

customCss

Replaces theme/custom.css: keyframes, grain, clip-paths, anything utilities can’t express. It is injected into every render and appended to emitted globals.css. The file is folder-wide, so theme has no effect on this channel. It’s a full replacement: read the current contents from get_theme’s customCss first when editing.

score_theme_contrast

Score WCAG contrast ratios for a theme’s salient color pairs (foreground on background, primary-foreground on primary, muted-foreground on background, …) in the light palette, the dark palette, or both.

ArgTypeRequiredDescription
modestringno"light" or "dark" to score one palette; default both
themestringnoNamed theme to score; default "default". An unknown name is a BadRequest listing the known themes

Returns { theme, summary: { total, passes, fails }, results } where each result carries label, mode, fg, bg, ratio, and a tier: AAA, AA, AAlarge, or Fail. Use it after reseeding a palette or tuning dark tokens.

add_theme

Create a named theme (theme/<name>.json) by cloning an existing one. Returns { name, theme }.

ArgTypeRequiredDescription
namestringyesLowercase kebab, not "default"
fromstringnoSource theme to clone; default "default"
overwritebooleannoDefault false; without it an existing name is refused

Boards pin a named theme via update_board with patch: { theme: "<name>" }; screenshot, render_snippet, and compare_to_url accept a theme param, and a screenshot of a screen on a pinned board uses that board’s theme by default. Edit a named theme afterwards with set_theme and its theme param.

update_theme

Rename a named theme and repoint every board that pinned it. Returns { name, repointedBoards }. To change a theme’s tokens, use set_theme with its theme param.

ArgTypeRequiredDescription
namestringyesTheme to rename
renameTostringyesNew name: lowercase kebab, not "default", not already taken

The default theme can’t be renamed, and an unknown name is refused (InvalidThemePath).

remove_theme

Delete a named theme. Returns { removedTheme }.

ArgTypeRequiredDescription
namestringyes

Refuses while any board still pins the theme, naming those boards; repoint or unpin them first with update_board (patch: { theme: null }). The default theme can’t be removed.

list_themes

List named themes and which boards use each. Takes no arguments. Returns { themes: [{ name, usedByBoards }] }; boards with no pin count toward default.

import_theme

Code-to-design: seed the theme from a host app’s stylesheet instead of picking colors by hand. Apply this before porting screens so copied classes render.

ArgTypeRequiredDescription
cssstringone of css/cssPathStylesheet text (an empty string counts as not provided)
cssPathstringone of css/cssPathPath to the app’s globals.css, either absolute or relative to the host app root (globals.css normally lives outside the design folder); the design folder’s parent and then the design folder are tried next
themestringnoNamed theme to merge into; default "default"
applybooleannoPersist the merge. Default false; dry-run
tailwindConfigPathstringnoPath to the app’s tailwind.config (absolute, or relative to the design folder). Auto-detected near cssPath when omitted

What it parses:

  • shadcn-convention custom properties: :root / .dark vars like --background (raw HSL triplets or any CSS color), with var() indirection resolved
  • Tailwind v4 @theme --color-* vars, plus --radius and --font-* roles
  • every non-semantic color var (numeric scales like --primary-600, extra roles like --success-500, bare brand names like --paprika and --ink) into the theme’s palette, so verbatim app classes like bg-primary-600 or bg-ink resolve literally on the canvas

It also ingests the tailwind.config’s theme.extend (given explicitly, or found near cssPath): brand colors to palette, named spacing to spacing tokens (so w-icon-rail / h-header resolve), boxShadow to shadows, fontFamily to font roles, and keyframes + animation to --animate-* utilities. CSS-derived values win over config literals of the same name. The app’s container config (center/padding/max-width) is applied so class="container" matches the app, and the result reports it as container: { detected, suggestedClasses, note }.

Slots the CSS doesn’t declare keep their current values. Returns { applied, changeCount, coverage, changes, warnings }. changes is [{ token, from, to }], and coverage counts how many semantic slots mapped versus how many values went to the palette passthrough, with a one-line summary. A dry-run adds a note; pass apply: true to persist. When the host app depends on a client chart library, the result carries detectedChartLibs and a chartHint to register the app’s chart components as render: "live" extensions.