Upgrading to v2
What changed in Panda v2, how to install it, and the breaking changes to fix as you migrate.
v2 keeps the framework you already know and rewrites the compiler underneath it. You write the same css(), recipes,
patterns, tokens, conditions, and JSX props. What changed is how Panda turns them into CSS.
What v2 is
v1 ran extraction and evaluation through ts-morph and ts-evaluator in Node. v2 replaces that hot path with a native
engine built on Oxc (opens in a new tab), shipped two ways:
@pandacss/compiler— a native binding. The CLI and bundler plugins use it.@pandacss/compiler-wasm— the same engine compiled to WASM for the browser. The playground runs on it.
Both wrap the same Rust crates, so Node and browser builds produce the same CSS. You get faster extraction — one parse
per file, no TypeScript program in the hot path — and a smaller install, since the ts-morph dependency tree is gone.
Output stays in parity with v1 except for the deliberate changes below.
See Why Panda → How it works for the model it keeps.
Install
Most projects only need @pandacss/dev:
pnpm add -D @pandacss/dev@latestUpgrade any integration you use to the same version: @pandacss/postcss, @pandacss/vite, @pandacss/webpack,
@pandacss/rollup, or @pandacss/bun. All @pandacss/* packages move on one version. Don't mix a v1 package with a v2
one.
v2 is ESM only and needs Node 22 or newer. Set "type": "module" (or use .mjs) so your panda.config.ts loads as
ESM.
Build
Your v1 panda.config.ts carries over. The first time, regenerate from a clean output folder:
panda build --clean # codegen + cssgen in one pass
panda codegen --clean # or, if you only run codegen
panda dev # rebuild on changeYou only need --clean once. v2 doesn't delete files it no longer generates, and by default v1 wrote .mjs where v2
writes .js. Vite resolves index.mjs before index.js, so a leftover v1 file can win over the new one.
Missing Flex or other pattern components after that? Add @pandacss/preset-base. See
Presets aren't added for you.
The panda and pandacss binaries are the same as v1.
Breaking changes to fix
These are the changes you have to act on when you move a project from v1.
ESM only
There's no CommonJS build. If your config or tooling used require():
// ❌ v1
const { defineConfig } = require('@pandacss/dev')
// ✅ v2
import { defineConfig } from '@pandacss/dev'Set "type": "module", use .mjs, or run through an ESM-aware bundler. The postcss.config.cjs that
panda init --postcss writes is CommonJS on purpose and still works.
Hooks moved to plugins
Hooks still exist, but they live on named plugins now, not a root hooks object:
// ❌ v1
export default defineConfig({
hooks: {
'cssgen:done': ({ content }) => content
}
})
// ✅ v2
export default defineConfig({
plugins: [
{
name: 'local',
hooks: {
'parser:before': {
filter: { id: '**/*.{jsx,tsx}' },
handler: ({ content }) => content
}
}
}
]
})Supported hooks: config:resolved, preset:resolved, parser:before, codegen:prepare, codegen:done, and
cssgen:done. cssgen:done is observe-only now — it runs after the final CSS with { artifact, content, path? } and
can't rewrite the string. Used it to strip unused tokens or keyframes? Reach for optimize.removeUnusedTokens /
removeUnusedKeyframes instead. The v1 engine hooks (context:created, parser:after, tokens:created,
utility:created, parser:before.configure(...), …) are gone. See Plugins.
createStyleContext is now two helpers
createStyleContext is gone from styled-system/jsx. Use one helper per recipe kind:
// ❌ v1 — one helper for both
import { createStyleContext } from 'styled-system/jsx'
// ✅ v2 — slot recipe (sva)
import { createSlotRecipeContext } from 'styled-system/jsx'
const { withRootProvider, withProvider, withContext } = createSlotRecipeContext(card)
// ✅ v2 — config recipe (cva)
import { createRecipeContext } from 'styled-system/jsx'
const { withContext } = createRecipeContext(button)withRootProvider is new — use it for a slot recipe's root when the root doesn't render a slot of its own. See
slot recipe context.
defineParts is gone
defineParts only renamed keys to selectors, so write the selector yourself. For a class per part, use
defineSlotRecipe instead.
// ❌ v1
const parts = defineParts(anatomy.build())
base: parts({ control: { borderWidth: '1px' } })
// ✅ v2 — the selector is the key
base: { '& [data-part="control"]': { borderWidth: '1px' } }If you'd rather keep the helper, copy it into your config:
import type { SystemStyleObject } from '@pandacss/types'
const defineParts =
<T extends Record<string, { selector: string }>>(parts: T) =>
(config: Partial<Record<keyof T, SystemStyleObject>>): SystemStyleObject =>
Object.fromEntries(Object.entries(config).map(([key, value]) => [parts[key].selector, value]))MCP moved to its own package
The MCP server left the CLI. Run it from @pandacss/mcp with the panda-mcp binary:
# ❌ v1
panda mcp
panda init-mcp
# ✅ v2 — run it directly, nothing to install
npx -y @pandacss/mcpSee MCP server.
Compound variants are their own class
v1 merged compound-variant styles into the css prop, so a textStyle in css replaced the compound's text style. v2
emits each compound variant as its own class, so properties you don't override still apply. If you relied on the
replacement, override those properties in css directly.
Packages folded into the compiler
These v1 internals are gone; their work lives in @pandacss/compiler now. Drop direct imports of @pandacss/core,
@pandacss/extractor, @pandacss/generator, @pandacss/node, @pandacss/parser, @pandacss/token-dictionary,
@pandacss/is-valid-prop, @pandacss/logger, @pandacss/reporter, and the Astro @pandacss/studio, replaced by the
hosted Studio (opens in a new tab). The same goes for @pandacss/plugin-vue and @pandacss/plugin-svelte:
the compiler reads .vue, .svelte, and .astro files directly, so remove them from plugins. If you only use
@pandacss/dev plus Vite or PostCSS, you're fine.
Config options removed
Nine options are gone: studio (and its sub-options), eject, emitTokensOnly, gitignore, clean, watch, poll,
lightningcss, and browserslist. forceConsistentTypeExtension is replaced by forceImportExtension — different
semantics, not a rename. outExtension gains a 'ts' value. See Config.
Presets aren't added for you
v1 added @pandacss/preset-base unless you set eject: true, even when you listed your own presets. v2 adds nothing. A
config that only lists its own presets loses the base utilities and conditions. It still builds, but with
unknown_condition warnings. List the presets you need:
// ❌ v1: preset-base was added for you
export default defineConfig({ presets: ['@acme/preset'] })
// ✅ v2
export default defineConfig({ presets: ['@pandacss/preset-base', '@acme/preset'] })theme.containerSizes is now theme.containers
The old key is ignored without a warning, so every @name/size container condition stops working.
// ❌ v1
theme: {
extend: {
containerSizes: {
sm: '384px'
}
}
}
// ✅ v2
theme: {
extend: {
containers: {
sm: '384px'
}
}
}Type imports moved
@pandacss/dev only exports the Config, Preset, and UserConfig types. Import other config types from
@pandacss/types:
// ❌ v1
import type { RecipeConfig } from '@pandacss/dev'
// ✅ v2
import type { RecipeConfig } from '@pandacss/types'Generated types changed too:
TokenandSizeTokenmoved fromstyled-system/tokenstostyled-system/types.types/prop-type(UtilityValues) andCssPropertiesare gone. UseSystemStyleObjectorSystemPropertiesfromstyled-system/types.- The generated
RecipeConfigandSlotRecipeConfigare nowRecipeDefinitionandSlotRecipeDefinition, without the config fields (className,jsx,staticCss,description). Use@pandacss/typesfor the config shape.
Template literal syntax is removed
The syntax option and the template-literal authoring mode are gone. Drop syntax from your config and the
--syntax flag from panda init, and write styles as objects:
// ❌ v1
css`
color: red;
`
// ✅ v2
css({ color: 'red' })Array conditions are deprecated
Array conditions still work: Panda converts them to the object form and warns. Move to an object that nests each rule
and ends in '@slot':
// ❌ v1
conditions: {
supportHover: ['@media (hover: hover)', '&:hover']
}
// ✅ v2
conditions: {
supportHover: {
'@media (hover: hover)': { '&:hover': '@slot' }
}
}Qwik JSX is removed
jsxFramework: 'qwik' no longer generates styled, Box, or pattern components. Drop the option and style with
css(), cva(), and pattern functions; Qwik's class accepts their output directly.
// ❌ v1
import { Box } from 'styled-system/jsx'
;<Box color="red.500" />
// ✅ v2
import { css } from 'styled-system/css'
;<div class={css({ color: 'red.500' })} />CLI commands and flags
Check your package scripts as well as your config. The CLI no longer accepts several v1 commands and flags. Use
panda doctor to check your setup, and add --json for scripts.
Replace removed commands
| v1 command | v2 replacement |
|---|---|
panda spec | panda codegen --spec, or --spec=./design-system.json for a custom path |
panda ship | panda lib to publish a design system; panda buildinfo --outfile ./dist/panda.buildinfo.json for build info alone |
panda emit-pkg | Removed. panda lib manages design-system package exports; review other runtime package exports yourself |
panda studio (including --build and --preview) | Use the hosted Studio (opens in a new tab) |
--spec also works on panda build and panda lib. It writes one design-system document instead of the v1 spec files.
See Codegen.
Replace logging and profiling flags
--silent is now --log-level silent. Update scripts such as:
// v1
{ "scripts": { "prepare": "panda codegen --silent" } }
// v2
{ "scripts": { "prepare": "panda codegen --log-level silent" } }Use --log-level error if you want errors to remain visible. silent also suppresses normal diagnostic output. The
available levels are silent, error, warn, info, and debug.
--profile replaces --cpu-prof and includes time in the Rust engine. It writes trace.json and timings.json, not a
Node .cpuprofile; update any scripts that read the old profile. Shared flags use kebab-case (--max-warnings,
--watch-debounce, …). See CLI.
Replace positional globs with --include
panda, panda cssgen, panda analyze, and panda debug no longer take a positional glob. Use --include, and
repeat it for more globs:
# v1
panda cssgen "src/**/*.tsx"
# v2
panda cssgen --include "src/**/*.tsx" --include "components/**/*.tsx"A positional glob on bare panda is treated as a command name. On cssgen, analyze, and debug, it does not
restrict the scan; Panda uses your configured include list instead.
panda cssgen tokens (and preflight, static, global, or keyframes) no longer selects an individual artifact.
Generate the full stylesheet, or use panda cssgen --splitting and import the layers you need.
Update output and build flags
| v1 flag | v2 replacement |
|---|---|
panda -o dist | panda --outdir dist. On the full build, -o now means --outfile, a CSS file path |
panda --preflight, --hash, --exclude / -e | Set preflight, hash, and exclude in panda.config.ts |
panda --minify / -m | Set minify in config, or run panda cssgen --minify |
panda cssgen -m | Use the full --minify flag |
panda cssgen --clean | Use panda build --clean or panda codegen --clean to clean generated output |
--poll / -p | Remove it; watching has no polling flag. --watch-debounce changes rebuild timing, not polling |
--lightningcss, --emitTokensOnly | Remove them; these flags have no v2 equivalent |
panda cssgen --minimal now includes staticCss rules, because they live in the recipes and utilities layers.
Per-package sheets built with --minimal repeat the static rules the host sheet already ships.
Theme keys without extend replace less than they did
A theme key written without extend still replaces what a preset defined. What changed is how much it claims.
v1 claimed the key directly under theme, so naming one scale claimed all of tokens:
theme: {
tokens: {
colors: {
brand: {
value: '#EA8433'
}
}
}
}- v1: color tokens replaced, and
spacing,fonts,radiiandfontSizesdropped with them. - v2: color tokens replaced, every other scale untouched.
v2 claims the entry you name. The same holds a level down:
theme: {
recipes: {
button: myButton
}
}- v1:
buttonreplaced, and every other recipe a preset registered dropped with it. - v2:
buttonreplaced, the rest left alone.
breakpoints is the exception. A breakpoint is a single value, not a definition, so writing them without extend
replaces the whole set.
Nothing to change if you already use extend.
Border overrides sort by property, not source order
v2 orders atomic rules by property breadth, deterministically. All the border shorthands sit in one tier, so an all-sides shorthand always wins over a per-side one, no matter the merge order. Composing an all-sides border with a per-side override no longer opens that side:
// v1: renders an open bracket — the override was declared last
// v2: renders a closed box — borderWidth re-applies the inline-end side
cx(css({ borderWidth: '1px', borderStyle: 'solid' }), css({ borderInlineEnd: '0' }))Reach for the longhand when the override has to win — longhands rank above every shorthand, so they always land last:
cx(css({ borderWidth: '1px', borderStyle: 'solid' }), css({ borderInlineEndWidth: '0' }))Padding, margin, and every other property group sort the same way. See Border.
scrollbarWidth takes keywords, not tokens
v1 mapped scrollbarWidth to sizes tokens, so scrollbarWidth: '4' emitted var(--sizes-4) and browsers dropped it.
It's now auto | thin | none:
// ❌ v1 — type-checked, invalid CSS
css({ scrollbarWidth: '4' })
// ✅ v2
css({ scrollbarWidth: 'thin' })If you passed a single color to scrollbarColor, move it to scrollbarThumb. scrollbarColor is now a raw two-value
string ('red transparent').
sizes.prose is narrower
@pandacss/preset-panda changes sizes.prose from 65ch to 60ch. Styles such as maxWidth: 'prose' will use the
narrower value. To keep the v1 width, override the token:
export default defineConfig({
theme: {
extend: {
tokens: {
sizes: {
prose: { value: '65ch' }
}
}
}
}
})No universal variable reset
v1 seeded --translate-x, --blur, --gradient-from-position and friends through a *, ::before, ::after, ::backdrop
rule — 34 declarations on every element, used or not. v2 registers those variables with @property instead, so they
carry their own defaults and only ship when you use the utility. A page that uses none of them gets an empty base layer.
This needs @property (Chrome 85+, Safari 16.4+, Firefox 128+); older browsers drop the affected utilities rather than
mis-render them. Set optimize.propertyFallback: true to also seed the defaults as plain declarations for the variables
your project uses.
Panda follows imports
In v1, a style value imported from another file produced no CSS. Panda saw brand, couldn't follow the import, and
skipped the property. v2 follows the import, reads the value, and generates the rule.
theme.ts
export const brand = 'red.500'
export const gutter = '4'button.tsx
import { css } from '../styled-system/css'
import { brand, gutter } from './theme'
// ❌ v1: no CSS for either property
// ✅ v2: background: var(--colors-red-500) and padding-inline: var(--spacing-4)
css({ bg: brand, px: gutter })Panda follows named imports, re-exports, export * barrels, and tsconfig path aliases. Imported helper functions and
css.raw() styles work too, as long as the result is static.
If you added staticCss entries or copied a value inline only because v1 couldn't see the import, you can remove them.
Two forms still aren't followed: default imports (import theme from './theme') and namespace imports
(import * as theme from './theme'). Use a named import instead. Values that only exist at runtime, like props or
state, never produce CSS from any file. See
Imports from other files.
What's new you'll want
Beyond parity, v2 adds features worth turning on:
- The
optimizeblock. Opt-in CSS cleanup:removeUnusedTokens,removeUnusedKeyframes,smartCompoundVariants,treeshakeDesignSystem, andpropertyFallback. It replaces the common v1cssgen:donecleanup. - New utilities and conditions. Mask helpers (Masks), scrollbar utilities, pointer and
validity conditions (
_pointerFine,_userValid,_inert), and raw CSS keywords liketextWrap: 'pretty'andjustifyContent: 'safe center'. viewTransition(). Style the View Transitions API and get a stable class back. See View transitions.positionTry(). Define CSS anchor-positioning fallbacks with a typed, tree-shaken factory. Replaces the removedglobalPositionTryconfig. See Position try.- Source transforms. Let bundler plugins rewrite static
css()calls withtransform: true. - Smaller
.d.ts.cva/svareturn types key on a clean props type, which unblocksisolatedDeclarations. See Isolated declarations. - Design systems.
panda libpublishes a component library; apps consume it with thedesignSystemconfig field. It replacespanda ship. See Build a design system and Consume with Panda. - Linting on the v2 engine. The ESLint & oxlint plugin lints against the same extraction the build uses.
See also
- Config — the full v2 config reference.
- CLI — every command and flag.
- Why Panda → How it works — the mental model v2 keeps.