Your design system
How design-system.json is structured — colours, scales, patterns and animations — and what each section generates.
design-system.json is the only file you edit. Everything the toolchain emits is derived from it,
and the JSON Schema is published so editors give you autocomplete and hover docs:
{
"$schema": "./node_modules/@getvitops/generator/schema.json"
}
vitops init stamps that for you.
What each section drives
| Section | Generates |
|---|---|
colors |
11-step OKLCH scales per hue, functional role tokens, automatic dark mode, colour utilities |
typeScale / spaceScale |
Fluid modular scales as clamp() steps |
typography |
Font families and semantic type roles → font-<role> utilities |
patterns |
Component CSS (.cta, .btn, .card, .badge, …) plus the token cascade |
animations |
Effect and journey classes, layered over the hand-written animation engine |
shadows |
--shadow-<name> tokens and .drop-shadow-<name> utilities |
The reference section documents every field, rendered from the schema itself — so it can’t fall out of step with what validation accepts.
Colours are seeded, not enumerated
You give a hue a seed; the generator derives the full scale and the functional tokens over it.
{
"colors": {
"palette": { "pine": { "seed": "#4A9075" } },
"roles": { "brand-primary": "pine", "ui-primary": "pine" }
}
}
That yields --color-pine-50 … 950 on the shared lightness ladder, plus role tokens named
--color-<target>-<role>[-<variant>] (--color-bg-brand-primary-solid,
--color-text-on-brand-primary, --color-text-brand-primary, …) and matching utilities —
the class name is the token name minus --color-. Dark mode re-points which step each token
reads, not a second palette you maintain. Contrast targets — text at APCA Lc ≥ 75, secondary
≥ 60, icons and boundaries ≥ 45, in both appearances — are enforced at build time, so a
violation fails generate rather than shipping.
A role is either kind: "surface" (a page or panel colour, with a bare bg-<role>) or
chromatic — the default, and what the bare-string form above means — whose backgrounds split
into tints and solids with no bare bg-<role>.
See Colour system for the full model.
Patterns are declarative
A pattern is base declarations, interaction states, and semantic role variants:
{
"cta": {
"group": "control",
"class": "cta",
"fill": true,
"default_role": "ui-primary",
"overrides": { "p": "0.75em 1.5em" },
"base": { "padding": "var(--p-cta-group)", "font-weight": "600" },
"states": { "hover": { "step": 1, "lift": "1px" }, "active": { "scale": 0.97 } },
"roles": ["success", "danger", "warning", "info"]
}
}
Two things worth internalising:
- Geometry resolves through the group alias layer. Write
var(--p-cta-group), notvar(--p-control, 0.75em). Both render the same, but the first keeps the pattern → group mapping in CSS where you can inspect and change it in devtools. The chain is--p-cta(your override hook) →--p-cta-group→--p-control→--p-default. element+classemit one zero-specificity rule."element": "button", "class": "btn"produces:where(button, .btn), so a bare<button>is styled with no class,.btncarries the styling to any other tag, and any explicit class overrides it without!important.
See Component patterns for the full cascade.