Skip to content

Velloo documentation

Markdown for LLMs ↗

Themes and tokens

One unified token tree, projected onto each framework, plus presets, contrast scoring, named themes, fonts, typesets, and custom CSS.

Velloo’s theme model is one tree, many projections. A single token source (colors, typography, spacing, radius, shadows) lives at theme/default.json, and each framework adapter projects it into its native form: shadcn gets CSS variables in the --primary / --primary-foreground convention, MUI gets a real createTheme() object, Chakra an extendTheme() object, Ant Design a theme config, and a no-framework/no-Tailwind folder gets plain CSS variables its inline styles reference.

Agents edit the theme through one operation, set_theme. It takes any combination of channels in a single call: tokens (dot-path patches), from (reseed from a preset or a seed color), fonts, typeset, and customCss, plus an optional theme to target a named theme. get_theme reads it back.

The token tree

{
  "name": "default",
  "colors": {
    "background": "oklch(1 0 0)",
    "foreground": "oklch(0.145 0 0)",
    "primary": { "DEFAULT": "oklch(0.55 0.159 275.206)", "foreground": "oklch(0.985 0 0)" },
    "muted":   { "DEFAULT": "oklch(0.97 0 0)", "foreground": "oklch(0.556 0 0)" },
    "border": "oklch(0.922 0 0)",
    "ring": "oklch(0.708 0.079 275.206)"
  },
  "colorsDark": {
    "background": "oklch(0.145 0 0)",
    "primary": { "DEFAULT": "oklch(0.68 0.16 275.206)", "foreground": "oklch(0.985 0 0)" }
  },
  "typography": {
    "fontFamily": { "sans": "Inter, ui-sans-serif, system-ui, sans-serif", "mono": "ui-monospace, SFMono-Regular, monospace" },
    "typesets": {
      "default": { "size": "1em", "leading": 1.75, "flow": "1.25em", "fontBody": "sans", "fontMono": "mono" },
      "reading": { "size": 18, "leading": 1.9, "flow": "2em" }
    }
  },
  "spacing": { "4": 16, "8": 32 },
  "radius": { "md": 6, "lg": 8 }
}

Color slots follow shadcn’s semantics: a slot is either a single CSS color (hex, rgb(), oklch(), …) or a DEFAULT / foreground pair. Patch any of them with set_theme’s tokens, for example { "colors.primary.DEFAULT": "#4f46e5" }. Beyond the semantic slots, an optional palette map captures raw scales imported from a host app (primary-600, success-500) so verbatim classes resolve during a port, and shadows, container, keyframes, and animation round out what an app’s Tailwind config can contribute.

OKLCH, not HSL

Every color Velloo generates is OKLCH (slots you set yourself can use any CSS color). Unlike HSL, OKLCH lightness is perceptually uniform (two colors at the same L actually look equally light), which is what makes programmatic palette work trustworthy: lightness scales stay even across hues, and contrast math behaves.

set_theme with from: { seedColor } takes one seed color (hex, rgb(), oklch(), anything parseable) and generates the full palette: lightness varied per slot, chroma preserved on primary/accent and reduced on neutrals, and every foreground checked against its background, nudged toward black or white until it clears WCAG AA (4.5:1).

Dark mode

colorsDark holds the dark palette as an explicit second tree: only the slots that differ; anything missing falls back to colors. (Raw brand scales work the same way: palette and an optional paletteDark.) Reseeding from a color builds light and dark from the same seed (a brightened primary on a near-black ground), and tokens patches under colorsDark.* make targeted dark edits. On the verification side, screenshot mode: "compare" renders light and dark side by side, and mutation and screenshot results carry diagnostics that flag raw colors which won’t flip with the mode, which is why “use semantic tokens” is the standing advice: bg-background flips with the mode, bg-white doesn’t. A frame can also pin scheme: "dark" so a board shows both at once (see Core concepts).

Presets

Twelve curated presets ship in the binary, each a complete token tree (fonts and typesets included), so set_theme with from: { preset } swaps wholesale:

default-light, default-dark, violet, emerald, amber, rose, indigo, ocean, slate, forest, sunset, plum

(These are the runtime presets behind set_theme’s from; velloo init --theme-preset has its own smaller starter list, led by the elsewhere palette the welcome sample uses.) Channels passed in the same call apply after the reseed, so a tokens patch alongside from overrides the new palette.

Contrast scoring

score_theme_contrast computes WCAG ratios for every salient pair (foreground/background, primary/primary-foreground, muted-foreground/background, and so on) in both light and dark palettes, and assigns each a tier:

TierMeaning
AAAratio ≥ 7
AAratio ≥ 4.5
AAlargeratio ≥ 3, passes only for large text
Failbelow 3

The canvas theme panel renders the same scores inline. Run it after any reseed; pass theme to score a named theme.

Named themes and board pinning

theme/default.json is the active theme, but a folder can hold several: add_theme clones one to theme/<name>.json, set_theme edits it through its theme parameter, boards pin one via update_board with patch: { theme }, and renders and screenshots accept a theme parameter. update_theme renames a theme and repoints the boards that pin it; remove_theme refuses while any board still does. That’s how you keep a light-brand marketing board and a dark-brand app board in one folder, or A/B two directions on adjacent boards.

Fonts

set_theme’s fonts channel assigns font families by role: each entry is { role, family, fallback?, google? }, and each role becomes a --font-<role> variable and a matching font-<role> utility (so a display role yields font-display). Pass google (an axis spec such as "wght@400..900", or true for a plain load) and the family loads in design mode and emits as an @import in the generated stylesheet. remove: true drops a role, and is refused while a typeset still names it. Roles are the lever for typographic personality: declare one per voice, not one per screen.

{ "fonts": [{ "role": "display", "family": "Fraunces", "google": "wght@300..900" }] }

Typesets

There is no font-size scale in the theme. Type rhythm comes from typesets in typography.typesets: each has three controls, size (base text size), leading (unitless body line-height), and flow (space between blocks), plus the font roles to use for body, headings, and code (fontBody, fontHeading, fontMono). The heading ladder, body and small copy, and block spacing all derive from those numbers, so changing leading reflows every heading consistently.

default is the folder baseline, projected onto :root, so Heading levels and Text on every screen follow it. Any other name, such as reading or compact, becomes a .typeset-<name> preset that a region opts into with the Prose helper (<Prose preset="reading">): headings, paragraphs, lists, quotes, code, and tables inside it pick up that rhythm without per-node classes. Edit them with set_theme’s typeset channel:

{ "typeset": [{ "name": "default", "size": 15, "leading": 1.6, "fontHeading": "display" }] }

custom.css: the escape hatch

theme/custom.css holds anything tokens and utilities can’t express: keyframes, grain textures, clip paths, selection colors. Read it from get_theme (customCss) and replace it with set_theme’s customCss channel; it replaces the whole file, so read first. It’s injected into every render after the theme variables and appended to the stylesheet emit_theme writes for Tailwind folders, so the canvas and your app agree.

Seeding from an app

Porting an existing app? import_theme parses a stylesheet (shadcn-convention :root / .dark custom properties or Tailwind v4 @theme variables, plus a nearby tailwind.config’s brand colors, named spacing, shadows, font families, and keyframes) and maps it onto the token tree. It’s dry-run by default, returning a from/to change list; pass apply: true to persist. Undeclared slots keep their current values, so a partial stylesheet doesn’t blow away the rest of the theme.

Emitting

The design-side theme becomes app-side artifacts through emit_theme. What it writes depends on the folder’s framework:

  • shadcn and Tailwind v4 folders: globals.css (default app/globals.css, or cssPath) with the theme variables, Google Fonts imports, and custom.css; a sibling typeset.css that globals.css imports; and a tailwind.config.ts unless you pass cssOnly.
  • Tailwind v3 apps (detected from the target’s package.json, or forced with tailwind: 3): a velloo-theme.css of HSL-triplet variables written next to your globals file, never into it; a sibling velloo-typeset.css; and a velloo.preset.ts or velloo.preset.cjs for your config’s presets. The result’s notes list the one-time wiring.
  • MUI, Chakra, and Ant Design: a theme module (createTheme({...}) in theme.ts, extendTheme({...}) in chakra-theme.ts, or a theme config in antd-theme.ts) with a separate darkTheme export when the theme has colorsDark.

Every framework also gets a framework-neutral DTCG tokens.json. It’s diff-first (dry-run unless you pass apply) and never silently overwrites your files. The workflow is in the Theme guide; tool signatures are in the MCP theme reference.