Skip to Content
Vektos
Guides

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 as brand.600 or neutral.900.
  • Semantic (semanticTokens.colors): what a color is for, such as bg.surface, fg.muted or danger.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:

PrefixAllowed 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 typecheck

Now 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 token

The 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, shadows and more. Add the key inside defineTokens({...}), run prepare, and the matching CSS properties start accepting it. Note that width/height read sizes, not spacing.
  • Dark mode. Semantic values are { base, _dark }. Panda writes the dark values under the .dark class, so putting dark on <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 tokens and semanticTokens exports 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.tsbuildinfo hides token changes. After prepare adds or removes a token, tsc in apps/web can keep passing on the old types. Delete apps/web/tsconfig.tsbuildinfo (it is gitignored) or run tsc -b --force.
  • prepare does 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 passes tsc. The lint rule prefer-token blocks it in this repo, and the raw-values grader counts it in agent code.
  • Only colors have a semantic layer so far. Spacing, radii and font sizes are used directly.
  • fonts.sans is the system stack. It reaches the page through the .vektos rule in globalCss; 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.muted as a background) passes tsc and lint. Only the token-roles grader sees it, so nothing blocks it in the editor.

On this page