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/devConfigure 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 codegenpackages/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 libIt 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-appmonorepo (opens in a new tab) in panda-examples.