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.

You own the package. The apps that use it run Panda. Run panda lib, then point each app at the package.

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

This walkthrough uses a workspace package at packages/ds that ships TypeScript source directly, the usual shape when the consumers live in the same repo. Publishing covers the built dist shape for npm.

pnpm add -D @pandacss/dev

Configure the theme

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

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, import from the local styled-system. Consumers import the component from @acme/ds.

Set up package.json

Write the "." entry yourself. Do not list styled-system exports. 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"
  }
}

Run panda lib

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. 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.

panda lib also syncs package.json exports. Your "." entry stays, the rest is generated. 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"
    }
  }
}

Do not edit the generated subpaths. The next panda lib rewrites them.

Fallback files

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. For a built-only package where those paths don't ship, pass what you publish:

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

Point an app at it

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 re-extracts what the package already ships. The package loads through designSystem, nothing else.

Render a <Button> in the app and it comes out styled by the button recipe, with no Panda config for it 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.

See also

Edit this page on GitHubView as markdown
Last updated on