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

styling
compiler engine

The compiler engine

How Panda compiles your styles — a Rust engine built on Oxc, exposed to both Node and the browser.

Panda's compiler is a Rust engine built on Oxc (opens in a new tab). It reads your source, pulls out the styles, and emits atomic CSS in cascade layers — the same model How Panda works describes, running on native code.

If you're coming from an earlier version, the pipeline is what changed:

before   ts-morph walks a TypeScript AST   →   PostCSS generates the CSS
after    Oxc parses each file once         →   native Rust emits the CSS

Your styles and your output don't change (see Upgrading to v2 for the handful of deliberate CSS differences). The machine underneath does.

Two bindings, one engine

The engine is a set of Rust crates. Two bindings wrap it, so the same code runs in both places:

  • @pandacss/compiler — a native binding (NAPI). The CLI and the Vite, webpack, and Rollup plugins use it.
  • @pandacss/compiler-wasm — the same engine compiled to WASM, for the browser. The playground and editor tooling run on it.

Both talk to a filesystem trait rather than node:fs directly, which is what lets the engine compile to WASM at all. Node and browser builds produce the same CSS.

One long-lived compiler

The entry point is createCompiler(config). It returns a stateful compiler you hold onto, not a one-shot function.

The expensive work — turning your config into utilities, conditions, recipes, and a resolved token dictionary — happens once, when you create the compiler. The same instance is reused for every file you parse and every rebuild. You feed it files as they change (parseFile, parseFiles, parseFileSource) and ask it for CSS; the config isn't recompiled each time.

That's what makes panda dev cheap: the watcher re-parses only the files that changed and re-emits, against config state that's already in memory.

The pipeline: extract, encode, emit

Data flows one way. Each stage is a separate crate, and dependencies point in the direction of the arrows — nothing downstream reaches back up.

source → extract → encode → emit → CSS
  • Extract. Oxc parses each source file once and matches your style calls — css(), cva(), patterns, JSX style props, and the rest. One parse per file, no TypeScript program in the hot path. Static values (including local consts and imports from other files) are folded here.
  • Encode. Matched usage becomes atomic style records — one property/value/condition per atom, deduplicated.
  • Emit. Atoms, recipes, and your static CSS become real CSS rules, grouped into @layers and written natively. No PostCSS pass.

Codegen runs alongside this to write the styled-system output — the types and helpers you import. A façade crate owns the multi-file build and watch state and wires the stages together; it's the single entry point both bindings talk to.

What you gain

You write the same Panda — static analysis of your source, atomic CSS, cascade layers, tokens, recipes, conditions. The compiled engine changes what that costs:

  • One parse per file, and no ts-morph AST to build and hold.
  • A smaller install, since the ts-morph / ts-evaluator dependency tree is gone.
  • One shared compiler core behind the tooling. Diagnostics and lint read the same extraction the build does, rather than reimplementing it — see Diagnostics, the ESLint & oxlint plugin, and Editor & IDE tooling.

Still open: CSS optimization

The emitter writes CSS natively and minifies when you set minify: true, but full parity with LightningCSS minification is still open. If you rely on the last few percent of that minified output, check the result before you ship it.

See also