Skip to content

Want to skip the docs? Check out pandamastery.com - the best way to learn Panda CSS

Guides

JSON Spec

Read your design system as JSON to render tokens in your own docs site or Storybook.

panda codegen --spec writes styled-system/specs/design-system.json — every token, condition and theme in one file. It's there so you can render your own palette, spacing scale and type without reaching into styled-system internals.

Your CSS doesn't need the file, so it's opt-in:

panda codegen --spec            # styled-system/specs/design-system.json
panda codegen --spec=meta.json  # or wherever you want it

panda build --spec writes it as part of a full build.

The file is generated. Edit your tokens in panda.config.ts, not here.

Read the spec

It's a normalized index rather than a list of tokens, so don't walk it by hand. @pandacss/compiler-shared ships the reader:

import { parseDesignSystem, indexDesignSystem } from '@pandacss/compiler-shared'
import spec from 'styled-system/specs/design-system.json'
 
const parsed = parseDesignSystem(spec)
if (!parsed.ok) throw new Error(parsed.error)
 
const ds = indexDesignSystem(parsed.value)

parseDesignSystem takes the imported object or a raw string. It's what turns a schema change into a sentence rather than an undefined three components deep:

Document is schemaVersion 99; this build reads 1. Upgrade @pandacss/compiler-shared.

ds.categories() lists what's there. ds.view(category) returns render-ready rows:

ds.view('colors')
// [{ path: 'colors.red.500', name: 'red.500', value: 'oklch(63.7% 0.237 25.331)', cssVar: '--colors-red-500' }]

path is what the other methods take. name is what you type in css({ ... }).

spec.tokens[path].originalValue carries the base token's value before reference expansion or derivation, when Panda retains it. For example, a token referencing {colors.red.500} keeps that string while its resolved value is a color. The field is omitted when no original value was retained; it does not vary with the selected condition or theme.

Render a category

import { grid } from 'styled-system/patterns'
 
export function Colors({ ds }) {
  return (
    <div className={grid({ columns: 3, gap: '4', padding: '6' })}>
      {ds.view('colors').map(token => (
        <div key={token.path}>
          <div style={{ background: token.value, height: 64, borderRadius: 8 }} />
          <p>{token.name}</p>
          <p>{token.value}</p>
        </div>
      ))}
    </div>
  )
}

Swap 'colors' for any category ds.categories() reports. Every row has the same shape, so one component renders them all.

Theme-aware tokens

A semantic token carries one value per condition. ds.states(path) returns them, base first. A token that doesn't vary returns a single row, so you can render every token the same way:

ds.states('colors.fg')
// [{ token: 'colors.fg', value: '#ef4444' },
//  { token: 'colors.fg', condition: '_dark', value: '#2563eb' }]
export function SemanticColors({ ds }) {
  return (
    <div>
      {ds.view('colors').map(token => (
        <div key={token.path}>
          <p>{token.name}</p>
          {ds.states(token.path).map(state => (
            <div key={state.condition ?? 'base'} style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
              <div style={{ background: state.value, width: 24, height: 24, borderRadius: 6 }} />
              <span>{state.condition ?? 'base'}</span>
            </div>
          ))}
        </div>
      ))}
    </div>
  )
}

ds.variants() reports which theme and condition combinations actually carry overrides, so you can build a switcher without guessing. To resolve a whole category under one of them, pass it to view:

ds.view('colors', { condition: '_dark' })

Recipes, patterns and composition styles

The same file lists the rest of the system, each as a table keyed by name. A table the system doesn't use is left out.

ds.recipes().button
// { className: 'btn', variants: { size: { values: ['md', 'sm'], allowsBoolean: false } }, defaultVariants: { size: 'md' } }
 
ds.patterns().stack.properties.gap
// { kind: 'token', category: 'spacing', description: 'Space between' }
 
ds.composition('textStyles')
// { body: { description: 'Paragraph copy' }, 'heading.lg': {} }

slotRecipes(), keyframes() and colorPalettes() read the same way. A pattern property's kind is what typegen resolved for it: enum with its values, token with its category, property with the CSS property, or a primitive.

Where a token came from

The spec carries a sources section: every preset and config that contributed, and which one defined each token.

const owner = (path: string) => spec.sources.entries[spec.tokens[path].source]
 
owner('colors.bg.neutral') // { kind: 'preset', name: '@acme/foundations' }
owner('colors.accent') // { kind: 'config', file: 'panda.config.ts' }

Each token carries a source index into sources.entries, the same way a value points at a condition. A token Panda can't place has no source. Themes carry theirs too; conditions have no row of their own, so they sit in sources.conditions.

Ship it with a library

panda lib --spec writes the spec beside preset.mjs rather than into an app's styled-system:

panda lib --spec
# dist/panda/design-system.json

panda/lib.json records it as spec, so tooling that reads the manifest can tell whether a package shipped one. Keep it inside what you publish: a path "files" would drop is left out of the manifest with a warning.

Ship it when you want consumers to render your tokens without running Panda themselves. It's large next to the rest of panda lib output, and anyone who installs your preset can generate their own from preset.mjs, so it stays a choice.

See also

  • Studio renders the same file with no setup
  • Tokens for the token format itself
Edit this page on GitHubView as markdown
Last updated on