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

design systems
building a design system

Build a design system

Ship tokens, recipes, and components as one package. Apps opt in with designSystem.

You own the design system and the apps, and both run Panda. Run panda lib in the package, then point each app at it with designSystem: '@acme/ds'.

That replaces wiring presets, importMap, and include by hand. The app reuses the styles the design system already extracted, instead of scanning its source again.

Sharing only tokens, or styling apps that don't use Panda? See when to use each.

Choose how to distribute

You want to…UseGuide
Share a full design system between apps that all use Pandapanda lib + designSystemThis guide
Share only tokens, recipes, or patterns (no components)A presetPresets
Style apps that don't use PandaA prebuilt static CSS fileComponent library

Set up the monorepo

Keep the design system and its apps as separate packages. This walkthrough uses pnpm workspaces.

pnpm-workspace.yaml

packages:
  - 'packages/*'
my-design-system/
├── packages/
│   ├── ds/        # the design system
│   └── app/       # an app that consumes it
├── package.json
└── pnpm-workspace.yaml

Configure the theme

Create packages/ds/panda.config.ts. Define tokens, recipes, and patterns. Set jsxFramework if you ship JSX components.

packages/ds/panda.config.ts

import { defineConfig } from '@pandacss/dev'
 
export default defineConfig({
  presets: ['@pandacss/preset-base'],
  include: ['src/**/*.{ts,tsx}'],
  outdir: 'styled-system',
  jsxFramework: 'react',
  theme: {
    tokens: {
      colors: {
        brand: { value: '#facc15' }
      },
      radii: {
        md: { value: '0.375rem' }
      }
    },
    recipes: {
      button: {
        className: 'button',
        base: {
          display: 'inline-flex',
          alignItems: 'center',
          borderRadius: 'md',
          paddingInline: '4',
          color: 'black',
          backgroundColor: 'brand'
        }
      }
    }
  }
})

Author your components

Generate styled-system with panda codegen, then build components on top of it. Import from the local styled-system. Consumers import the components from your package name.

packages/ds/src/button/button.tsx

import type { ComponentPropsWithoutRef } from 'react'
import { button } from '../../styled-system/recipes'
 
export function Button(props: ComponentPropsWithoutRef<'button'>) {
  return <button className={button()} {...props} />
}

packages/ds/src/index.ts

export { Button } from './button/button'

Set up package.json

Write the package entry, the build scripts, and a peer dependency on @pandacss/dev. Don't list styled-system exports yourself. panda lib adds them.

packages/ds/package.json

{
  "name": "@acme/ds",
  "version": "1.0.0",
  "type": "module",
  "exports": {
    ".": "./src/index.ts"
  },
  "scripts": {
    "codegen": "panda codegen",
    "lib": "panda codegen && panda lib",
    "watch": "panda codegen && panda lib --watch"
  },
  "peerDependencies": {
    "@pandacss/dev": "^2.0.0",
    "react": ">=18"
  }
}

Publish the library artifacts

pnpm --filter @acme/ds lib

panda lib writes three files under dist/panda/:

  • lib.json is the manifest a consumer resolves as @acme/ds/panda/lib.json.
  • preset.mjs is your theme (tokens, recipes, patterns).
  • buildinfo.json is the extraction result, so apps don't scan your source again.

An app never imports preset.mjs or buildinfo.json by hand. designSystem loads both.

It also syncs package.json exports. Your "." entry stays. Everything else below is added for you, and only for categories codegen actually emitted. This walkthrough has no patterns, so ./patterns is absent.

packages/ds/package.json

{
  "name": "@acme/ds",
  "exports": {
    ".": "./src/index.ts",
    "./panda/*": "./dist/panda/*",
    "./css": {
      "types": "./styled-system/css/index.d.ts",
      "default": "./styled-system/css/index.js"
    },
    "./css/*": {
      "types": "./styled-system/css/*.d.ts",
      "default": "./styled-system/css/*.js"
    },
    "./helpers": {
      "types": "./styled-system/helpers.d.ts",
      "default": "./styled-system/helpers.js"
    },
    "./recipes": {
      "types": "./styled-system/recipes/index.d.ts",
      "default": "./styled-system/recipes/index.js"
    },
    "./recipes/*": {
      "types": "./styled-system/recipes/*.d.ts",
      "default": "./styled-system/recipes/*.js"
    },
    "./jsx": {
      "types": "./styled-system/jsx/index.d.ts",
      "default": "./styled-system/jsx/index.js"
    },
    "./jsx/*": {
      "types": "./styled-system/jsx/*.d.ts",
      "default": "./styled-system/jsx/*.js"
    },
    "./tokens": {
      "types": "./styled-system/tokens/index.d.ts",
      "default": "./styled-system/tokens/index.js"
    }
  }
}

Don't edit those subpaths by hand. The next panda lib rewrites them.

Ship fallback files

If a consumer can't use the shipped buildinfo.json (corrupt file, schema skew), Panda re-extracts the paths listed in lib.json as files. No files means that recovery fails closed.

panda lib infers those paths from your include globs. If package.json "files" would not publish them (a "files": ["dist"] package whose sources live in src/), Panda drops the inferred paths and warns with design_system_files_not_publishable. For a built-only package, pass the files you actually publish:

panda lib --files './**/*.{js,mjs}'

Build dist/panda/ before you publish. A "prepublishOnly": "panda codegen && panda lib" script keeps it fresh without committing generated output.

Point an app at it

The app writes one field. Bundler setup, imports, extensions, and prefixes live on Consume a design system.

packages/app/panda.config.ts

export default defineConfig({
  designSystem: '@acme/ds'
})

Develop with watch mode

pnpm --filter @acme/ds watch   # panda codegen once, then panda lib --watch
pnpm --filter @acme/app dev

panda lib --watch rewrites dist/panda/ when tokens, recipes, or components change. It does not rerun panda codegen. If a change updates generated styled-system files, run panda codegen again (or keep a second codegen watcher).

Version and publish

pnpm --filter @acme/ds lib
pnpm --filter @acme/ds publish

The design system and its apps must run the same Panda major. panda lib stamps that range from the package's @pandacss/dev peer, or from --panda. A mismatch fails the consumer build with design_system_peer_range_unsatisfied.

The manifest version field is informational. Panda does not enforce it. Use normal semver when you rename or remove tokens.

Troubleshooting

  • design_system_files_not_publishable means inferred fallback paths would not ship in the npm tarball. Pass --files for the sources you actually publish, or consumers can't recover from stale build info.
  • design_system_export_overwritten means panda lib replaced a package.json export that didn't match what it would write. Let it own the ./panda/* and styled-system subpaths.

Wire the app next: Consume a design system.