Skip to content

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

Build a design system

Run panda lib in the package. Apps opt in with designSystem.

A design system here is one package that ships both your components and the Panda config they were built from, tokens, recipes, all of it. Apps that consume it don't re-extract your source. They load a small manifest you publish alongside the package.

Build the package like a normal Panda project, run panda lib to publish that manifest, then add one field to each consuming app's config to point at it.

The command is the same in a monorepo and on npm. Only the workflow around it differs: Monorepo workflow covers watch mode, Publishing covers the npm tarball.

If your apps do not run Panda, ship a stylesheet instead: Consume without Panda. If you only need tokens, without components, ship a preset.

Create the package

Start with a workspace package, packages/ds here, that ships its TypeScript source directly. That's the normal shape when consumers live in the same repo as the package. Publishing to npm instead? Publishing covers the built dist shape.

Add Panda to the package:

pnpm add -D @pandacss/dev

Configure the theme

This is a normal panda.config.ts, nothing design-system-specific yet. Define the tokens and recipes your components will use, same as any Panda project:

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 a component

Generate the styled-system helpers, then write the component against them:

panda codegen

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'

Inside the package, Button imports from the local styled-system. Once published, consumers of this walkthrough just import Button itself from @acme/ds. An app that also wants your css() or cva() directly, to build its own components on the same tokens, can import those from @acme/ds/css too, covered in Consume with Panda.

Set up package.json

You write the "." entry, the package's main entry point. Leave every styled-system export out. panda lib generates and syncs those for you on every run. By hand, they'd drift out of date the moment you add a recipe:

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"
  }
}

Run panda lib

Run the lib script you just added:

pnpm --filter @acme/ds lib

It writes three files under dist/panda/:

dist/panda/
├── lib.json         # the manifest: theme, import map, fallback file globs
├── preset.mjs       # tokens, recipes, patterns
└── buildinfo.json   # the styles panda lib extracted from your source

An app never imports these files directly. Setting designSystem: '@acme/ds' loads the manifest, merges the preset into the app's theme, and replays buildinfo.json. The app gets your styles without re-scanning your source at all.

panda lib also syncs package.json exports for you. Your "." entry stays untouched. Everything else is generated fresh each run. Only categories your package actually uses show up, so this walkthrough has no patterns and ./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 hand-edit the generated subpaths. The next panda lib run overwrites them anyway.

Extend another design system

A design system can extend a parent package. Install the parent, then point the child config at it:

packages/marketing/panda.config.ts

import { defineConfig } from '@pandacss/dev'
 
export default defineConfig({
  designSystem: '@acme/foundations',
  include: ['src/**/*.{ts,tsx}'],
  outdir: 'styled-system',
  jsxFramework: 'react'
})

Run panda lib in the child package. Panda records the parent and tracks which exports each child module imports from it. Apps then configure only the leaf package, such as @acme/marketing.

After upgrading Panda, rebuild each intermediate design-system package with panda lib. Older build info stays correct by hydrating the full parent when that package is selected, but Panda cannot narrow the parent CSS until you rebuild it.

See Nested design systems for the app config.

Fallback files

"Stale" means buildinfo.json can't be read, doesn't match the running Panda version, or is malformed, not that your components changed since the last panda lib run. Here's what happens when a consuming app tries to hydrate it:

app reads buildinfo.json
├─ ok    → replays your styles, no re-scan
└─ stale → re-extracts the `files` globs in lib.json   (warns: design_system_buildinfo_stale)
    └─ no `files` → the build fails, nothing silently missing

panda lib infers files from your include, so the fallback usually just works. For a built-only package where those source paths don't actually ship (you publish dist, not src), tell it what you publish instead:

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

Point an app at it

This is the one field from the intro, written in a real app config:

packages/app/panda.config.ts

export default defineConfig({
  designSystem: '@acme/ds',
  include: ['src/**/*.{ts,tsx}']
})

Do not put the design system in include. Listing the package name there errors with design_system_in_include. A glob into its source would re-extract what the package already ships. The package loads through designSystem alone, nothing else.

Render a <Button> in the app and it comes out styled by the button recipe, with no Panda config for it anywhere in the app:

packages/app/src/app.tsx

import { Button } from '@acme/ds'
 
export function App() {
  return <Button>Save</Button> // brand background, md radius
}

Bundler setup, imports, and theme overrides: Consume with Panda.

Example

A workspace with the package and a Next.js app already wired:

npx degit chakra-ui/panda-examples/examples/monorepo my-app

monorepo (opens in a new tab) in panda-examples.

See also

Edit this page on GitHubView as markdown
Last updated on