Tokens
Declare a design value once and let the compiler enforce it.
What
A token is a named design value: accent.default instead of #2563eb, 4 instead of
1rem. All tokens live in one file, packages/ds/panda.config.ts. Panda turns them
into CSS variables and into TypeScript types, so css() accepts a token and rejects
everything else.
There are two layers of color:
- Palette (
tokens.colors): raw shades such asbrand.600orneutral.900. - Semantic (
semanticTokens.colors): what a color is for, such asbg.surface,fg.mutedordanger.text. Each has a light and a dark value that point into the palette. Components use only these.
Why
If the only values you can write are tokens, "does this code follow the design
system?" becomes a question the compiler can answer. That is the whole premise of
Vektos. strictTokens: true in the config is the switch that makes it real.
The semantic layer adds a second question the compiler cannot answer but a linter can:
"did you pick the token that means this?" brand.600 and accent.default are the same
blue today, but only the second one turns light blue in dark mode. Using the palette
directly is flagged by no-primitive-token (see 02-lint). Using a
semantic token for the wrong job, like fg.muted as a background, is the next level:
the token-roles grader checks it (see 03-graders).
Next to colors there are scales for spacing, sizes, radii, type (fonts, fontSizes,
fontWeights, lineHeights, letterSpacings), shadows and z-index, and breakpoints (sm 40rem, md
48rem, lg 64rem, xl 80rem; mobile first: { base: "column", md: "row" }). fonts has
two families, sans and mono; strictTokens makes any other fontFamily a type error.
The name carries the rule:
| Prefix | Allowed on |
|---|---|
bg.* | backgrounds |
fg.* | text and icons |
border.* | borders and outlines |
danger, success, warning | .default fills, borders, outlines and icons; .subtle backgrounds; .text text and icons |
accent | .default and .emphasized fills, and a border that continues the fill; .fg text and icons on that fill; .text text, icons, outlines and focus rings, never a fill |
The palette is mint and ink: green-tinted neutrals (neutral for the light theme, gray
for the dark one), one mint accent, and an ink that stays dark in both themes.
accent.default is mint (#85f4b1) in both themes: solid buttons, selected filters, the current
navigation item, with accent.fg, a near-black green, on it (11.6:1). Mint is too light to see on
a white page (1.3:1), so it is never text, an outline or a focus ring. accent.text does those
jobs: emerald (#0b6b3c) in light mode, mint in dark mode, at least 5.7:1 on every background.
Plain links (<a> without a class) get it, underlined, from a global rule; the focus ring, the
outline Button and the Select check mark use it too.
success is cyan, not green: a green "ok" would read as the mint accent.
Ink regions
bg.inverse is the ink (#080b09), the same in both themes. The app header and the tooltips
use it; table heads used to, and sit on bg.subtle since chapter 45. An ink region also carries the dark class, so everything inside it (text,
links, buttons, badges) takes its dark-theme colors without tokens of its own:
<header className={cx("dark", css({ bg: "bg.inverse", color: "fg.default" }))}>Set color on the region itself: text inherits the color computed on its parent, which is the
light one. Panda's dark condition is .dark &, so it holds on any element, not only <html>.
Try it
Add a semantic color to the config:
// packages/ds/panda.config.ts, inside semanticTokens.colors
info: {
subtle: {
value: { base: "{colors.brand.700}", _dark: "{colors.brand.400}" },
description: "Tinted background for informational messages.",
},
},Regenerate the types, then check:
bun run --filter @vektos/ds prepare # rewrites packages/ds/styled-system/
bun run typecheckNow in any component:
css({ bg: "info.subtle" }) // OK, and the editor suggests it after typing "i
css({ bg: "#1d4ed8" }) // TS2769: not assignable to ColorsValue
css({ bg: "brand.700" }) // compiles, but oxlint: use a semantic tokenThe editor suggestion list opens by itself inside the quotes. That comes from
.vscode/settings.json (editor.quickSuggestions.strings) and from using the
project's TypeScript 7 (js/ts.tsdk.path).
Extend it
- New category. Panda knows
colors,spacing,sizes,fontSizes,fontWeights,radii,borderWidths,shadowsand more. Add the key insidedefineTokens({...}), runprepare, and the matching CSS properties start accepting it. Note thatwidth/heightreadsizes, notspacing. - Dark mode. Semantic values are
{ base, _dark }. Panda writes the dark values under the.darkclass, so puttingdarkon<html>(or any ancestor) switches the theme for everything inside. Storybook has a toolbar button for it. - A new palette shade. Only when a semantic token needs it; the palette is small on purpose, so the question "which blue?" has few answers.
- Where else tokens are read. oxlint's Panda rules load the same config to suggest
the right token (see 02-lint). The Storybook "Tokens" page renders
the
tokensandsemanticTokensexports of the config, so it updates on its own.
Motion and layers
Two animations, spin (a Spinner) and pulse (a Skeleton), with their keyframes in
the theme. Nothing else in the design system moves, and both pause under
prefers-reduced-motion. zIndex has overlay, modal, popover and tooltip, in that
order; toasts sit above all of them through Zag's own inline z-index.
globalCss also holds a few raw values that are a library's mechanics, not a style: Zag
places each toast with CSS variables (--x, --y, --height, --opacity) that the
stylesheet must apply, and Ark draws its progress ring from --size and --thickness. They live in the config, the one file where raw values are allowed.
The CSS reset
Panda ships a CSS reset ("preflight"): no margins, no list bullets, borders set to
0 solid. We scope it to .vektos, so the design system only resets elements inside
that class and never touches the host page. Put the class on <html> in your app, as
apps/web/src/app/layout.tsx does; that also covers components rendered through a
portal. Storybook adds it with a decorator.
Known gaps
- A stale
tsconfig.tsbuildinfohides token changes. Afterprepareadds or removes a token,tscinapps/webcan keep passing on the old types. Deleteapps/web/tsconfig.tsbuildinfo(it is gitignored) or runtsc -b --force. preparedoes not run on save. After editing the config, run it by hand or the editor keeps showing the old tokens.- Panda's escape hatch
css({ bg: "[#dc2626]" })is allowed on purpose and passestsc. The lint ruleprefer-tokenblocks it in this repo, and theraw-valuesgrader counts it in agent code. - Only colors have a semantic layer so far. Spacing, radii and font sizes are used directly.
fonts.sansis the system stack. It reaches the page through the.vektosrule inglobalCss; before chapter 45 nothing applied it, and Panda's reset chose the font.- An ink region in dark mode is a little darker than the page, not lighter: it reads as a band, separated by its border.
- "Right token, wrong job" (
fg.mutedas a background) passestscand lint. Only thetoken-rolesgrader sees it, so nothing blocks it in the editor.