Skip to content

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

Overview

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@latest

Upgrade 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. Regenerate:

panda build    # codegen + cssgen in one pass
panda dev      # rebuild on change

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/mcp

See MCP server.

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.

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' })

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

panda inspect, panda validate, and panda info are removed. Use panda doctor (add --json for scripts).

Logging flags are consolidated: --log-level silent|error|warn|info|debug replaces --silent, --quiet, and --verbose. --profile replaces --cpu-prof and covers time in the Rust engine, not just the Node side. Shared flags are kebab-case (--max-warnings, --watch-debounce, …). See CLI.

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, radii and fontSizes dropped 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: button replaced, and every other recipe a preset registered dropped with it.
  • v2: button replaced, 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').

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.

What's new you'll want

Beyond parity, v2 adds features worth turning on:

  • The optimize block. Opt-in CSS cleanup: removeUnusedTokens, removeUnusedKeyframes, smartCompoundVariants, treeshakeDesignSystem, and propertyFallback. It replaces the common v1 cssgen:done cleanup.
  • New utilities and conditions. Mask helpers (Masks), scrollbar utilities, pointer and validity conditions (_pointerFine, _userValid, _inert), and raw CSS keywords like textWrap: 'pretty' and justifyContent: '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 removed globalPositionTry config. See Position try.
  • Cross-file composition and source transforms. Compose css.raw() styles across files, and let bundler plugins rewrite static css() calls with transform: true.
  • Smaller .d.ts. cva / sva return types key on a clean props type, which unblocks isolatedDeclarations. See Isolated declarations.
  • Design systems. panda lib publishes a component library; apps consume it with the designSystem config field. It replaces panda 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

Edit this page on GitHubView as markdown
Last updated on