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

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

See 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:

  • Token and SizeToken moved from styled-system/tokens to styled-system/types.
  • types/prop-type (UtilityValues) and CssProperties are gone. Use SystemStyleObject or SystemProperties from styled-system/types.
  • The generated RecipeConfig and SlotRecipeConfig are now RecipeDefinition and SlotRecipeDefinition, without the config fields (className, jsx, staticCss, description). Use @pandacss/types for 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 commandv2 replacement
panda specpanda codegen --spec, or --spec=./design-system.json for a custom path
panda shippanda lib to publish a design system; panda buildinfo --outfile ./dist/panda.buildinfo.json for build info alone
panda emit-pkgRemoved. 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 flagv2 replacement
panda -o distpanda --outdir dist. On the full build, -o now means --outfile, a CSS file path
panda --preflight, --hash, --exclude / -eSet preflight, hash, and exclude in panda.config.ts
panda --minify / -mSet minify in config, or run panda cssgen --minify
panda cssgen -mUse the full --minify flag
panda cssgen --cleanUse panda build --clean or panda codegen --clean to clean generated output
--poll / -pRemove it; watching has no polling flag. --watch-debounce changes rebuild timing, not polling
--lightningcss, --emitTokensOnlyRemove 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, 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').

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 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.
  • Source transforms. 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