Skip to content
Themes and tokens

Documentation

Velloo documentation

Markdown for LLMs ↗

Themes and tokens

One unified token tree, projected onto each framework, plus the design language, 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.

A theme is half of a design’s look. The other half is its rules in words, and the two together are the design’s design language.

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.

Presets are an agent’s and the CLI’s starting points. The canvas no longer has a preset gallery; it offers its own starting directions in the design-language stage.

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:

Tier Meaning
AAA ratio ≥ 7
AA ratio ≥ 4.5
AAlarge ratio ≥ 3, passes only for large text
Fail below 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.

A theme’s name is its file’s stem, kept as you typed it: letters of either case, digits, spaces, -, and _, so “Night Shift” is saved as theme/Night Shift.json. A name starts and ends with a letter or digit and runs to 48 characters at most. Names are unique without regard to case, because macOS and Windows keep Night.json and night.json as one file. The first theme is shown as “Default”, and default is never a name you can take.

On the canvas, the Theme rail’s New theme button asks for the name, then opens the new theme in the guided design-language flow. It starts as a copy of the current theme and stays a draft until you finish: Cancel removes it, and Done puts the board on it.

The design language

Tokens say what a design’s colours, type, and corners are. They cannot say “one filled button per view” or “no shadows on cards”. A design language is the theme plus those rules in words: what the design is going for, and its Do’s and Don’ts.

The canvas has a stage for it, a visual counterpart to a written DESIGN.md. It sets colour, type, shape, and the rules beside a specimen: the theme drawn on the design’s own components, light and dark. The specimen is a render, not a board or a screen. It is built on request, and nothing is written to the design folder for it.

  • Guided the first time, review afterwards. A first definition is four steps: Colour, Type, Shape, Rules. An existing language is laid out to move around in.
  • Edits are live, and Cancel puts them back. They are ordinary theme and rule writes, so every board that uses the theme updates as you go. Cancel restores the theme and the rules as they stood when you opened the stage.
  • It edits the theme the board uses, not always the default. The Theme rail and the stage share one set of colour, type, and shape controls.

The rules live in the document the design follows. That is the design folder’s own guidance.md, under Overview and Do's and Don'ts, which the stage and the set_design_language operation both write. When the repository has its own DESIGN.md, the design follows that instead: the stage shows its rules read-only, and Velloo never writes the file.

The model is that an agent proposes and you refine. Reading an existing app’s language needs an agent, so your coding agent proposes one during setup and you adjust it on the canvas. Starting fresh needs no agent. The workflow is in the Theme guide.

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" }] }

Radius

radius holds the corner scale. A theme that states a scale (sm, md, lg, xl) gets those steps as written. A theme that states only a base (md) gets the rest derived from it, the way shadcn derives its rounded-* steps from --radius. Either way the scale reaches Tailwind screens as --radius-sm, --radius-md, --radius-lg, and --radius-xl, so rounded-lg on the canvas follows the theme.

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.

Three details matter in practice:

  • Imported stylesheets are followed. A global stylesheet is often only an entry point. Given a path, the import follows its relative @imports inside the app and reads the tokens from the files that hold them.
  • --radius replaces the corner scale. It is the base the other steps derive from, so a stylesheet that states it replaces the theme’s scale with that base instead of leaving the old steps beside it.
  • What did not come across is listed. notImported names what the app states that the import could not carry, such as a font it could not trace or a dark palette the stylesheet does not define.

velloo init runs this same import when it creates a design in an app, including the fonts a next/font loader sets and, in a Mantine app, the app’s own 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.