Panda Integration Hooks
Six lifecycle hooks for changing config, transforming source before parsing, and observing generated CSS.
Panda runs your build in stages: resolve config, parse source, generate code, generate CSS. Hooks let you plug into each of those stages without forking Panda itself.
There's no root-level hooks option on your config. Every hook lives on a plugin, an object with a name and a
hooks map, listed in the plugins array:
import { defineConfig } from '@pandacss/dev'
export default defineConfig({
plugins: [
{
name: 'my-plugin',
hooks: {
'cssgen:done': ({ content, path }) => {
// ...
}
}
}
]
})That's deliberate: a hook attached to a named plugin is something you can copy into another project or publish on
npm. A bare hooks object on the config wouldn't be.
The six hooks
| Hook | Fires | Use it to |
|---|---|---|
config:resolved | After presets merge, before defaults apply | Strip or rewrite the resolved config |
preset:resolved | When a preset resolves, before configs merge | Filter what a preset contributes |
parser:before | After a file is read, before it's parsed | Rewrite non-TSX source into syntax Panda can parse |
codegen:prepare | Before generated files are written | Add or modify JS/.d.ts artifacts |
codegen:done | After generated files are written | React to the finished styled-system output |
cssgen:done | After final CSS is produced | Observe the CSS (read-only, can't rewrite it) |
The rest of this page walks through the four you'll actually reach for.
Rewrite the resolved config
config:resolved gets the fully merged config plus a set of utils for editing it. This example removes the stack
pattern:
import { defineConfig } from '@pandacss/dev'
export default defineConfig({
plugins: [
{
name: 'remove-stack-pattern',
hooks: {
'config:resolved': ({ config, utils }) => {
return utils.omit(config, ['patterns.stack'])
}
}
}
]
})Filter a preset before it merges
preset:resolved runs once per preset, before any of them merge into your final config. Use it to strip something
out of a preset you don't control, instead of forking it:
import { defineConfig } from '@pandacss/dev'
export default defineConfig({
plugins: [
{
name: 'trim-preset-colors',
hooks: {
'preset:resolved': ({ utils, preset, name }) => {
if (name === '@pandacss/preset-panda') {
return utils.omit(preset, ['theme.tokens.colors', 'theme.semanticTokens.colors'])
}
return preset
}
}
}
]
})Feed Panda a file it can't parse natively
parser:before receives { filePath, content } and returns the rewritten string. Return nothing and the content
passes through unchanged. This is the whole mechanism behind supporting a templating language Panda doesn't parse on
its own: strip or rewrite whatever Panda's parser would choke on, before parsing happens.
import { defineConfig } from '@pandacss/dev'
export default defineConfig({
plugins: [
{
name: 'strip-directives',
hooks: {
'parser:before': ({ filePath, content }) => {
if (!filePath.endsWith('.astro')) return
return content.replace(/^---[\s\S]*?---/, '')
}
}
}
]
})Scope a hook to a set of files by passing { filter, handler } instead of a bare function:
import { defineConfig } from '@pandacss/dev'
export default defineConfig({
plugins: [
{
name: 'scoped-parser',
hooks: {
'parser:before': {
filter: { id: '**/*.{jsx,tsx}' },
handler: ({ content }) => content
}
}
}
]
})Observe the CSS after it's built
cssgen:done fires once the CLI, Vite plugin, or PostCSS plugin has produced the final CSS. It's read-only: whatever
you return is ignored, so this hook is for reporting, not rewriting.
import { defineConfig } from '@pandacss/dev'
export default defineConfig({
plugins: [
{
name: 'css-report',
hooks: {
'cssgen:done': ({ artifact, content, path }) => {
if (artifact === 'styles.css') {
console.log(`Generated ${content.length} bytes at ${path}`)
}
}
}
}
]
})To actually strip unused tokens or keyframes from that CSS, you don't need a hook at all, optimize already does it:
import { defineConfig } from '@pandacss/dev'
export default defineConfig({
optimize: {
removeUnusedTokens: true,
removeUnusedKeyframes: true
}
})For any other CSS transform, run PostCSS after Panda instead of trying to do it from cssgen:done.
With removeUnusedTokens on, don't rely on token.var (or
token(xxx) for a semantic token path) from styled-system/tokens. Panda
decides which CSS variables to keep by scanning the generated CSS, and a variable only referenced through that JS
helper won't show up in the scan.
Sharing a hook
Since every hook lives on a plugin, sharing one is just exporting the plugin object:
import { defineConfig } from '@pandacss/dev'
const myPlugin = {
name: 'strip-stack',
hooks: {
'config:resolved': ({ config, utils }) => {
return utils.omit(config, ['patterns.stack'])
}
}
}
export default defineConfig({
plugins: [myPlugin]
})Plugins run in the order they appear in plugins, with your own config's own values applied last. Unlike presets,
a plugin can't be extended by the config that uses it, it just runs.
Reference
export interface PandaHooks {
/** Called after authored presets are merged, before defaults and serialization. */
'config:resolved': (args: ConfigResolvedHookArgs) => MaybeAsyncReturn<void | Config>
/** Called when an authored preset is resolved, before all configs are merged. */
'preset:resolved': (args: PresetResolvedHookArgs) => MaybeAsyncReturn<void | Config>
/**
* Called after reading file content but before parsing it.
* Use this to transform non-standard source into TSX-friendly syntax.
*/
'parser:before': (args: ParserResultBeforeHookArgs) => MaybeAsyncReturn<string | void>
/** Called before generated files are written by a JS host. */
'codegen:prepare': (args: CodegenPrepareHookArgs) => void | CodegenPrepareArtifact[]
/** Called after generated files are written by a JS host. */
'codegen:done': (args: CodegenDoneHookArgs) => void
/**
* Called after final CSS is produced by a JS host (observe-only; no rewrite).
* Fires for CLI, Vite, and PostCSS string sinks. Use `optimize` or PostCSS to mutate CSS.
*/
'cssgen:done': (args: CssgenDoneHookArgs) => void
}Each hook accepts a plain function, or { filter, handler } to scope it to specific files:
export type PandaHook<Handler> = Handler | { filter?: HookFilter; handler: Handler }Argument types:
export interface ConfigResolvedHookArgs {
config: Config
path: string
dependencies: string[]
utils: ConfigResolvedHookUtils
}
export interface PresetResolvedHookArgs {
preset: Config
name: string
utils: ConfigResolvedHookUtils
}
export interface ConfigResolvedHookUtils {
omit<T extends object>(obj: T, paths: string[]): T
pick<T extends object>(obj: T, paths: string[]): Partial<T>
traverse(obj: unknown, callback: (item: TraverseItem) => void, options?: TraverseOptions): void
}
export interface ParserResultBeforeHookArgs {
filePath: string
content: string
original?: string
}
export interface CodegenPrepareHookArgs {
artifacts: CodegenPrepareArtifact[]
outdir: string
cwd?: string
}
export interface CodegenDoneHookArgs {
files: string[]
outdir: string
cwd?: string
}
export interface CssgenDoneHookArgs {
artifact: 'styles.css' | 'styles.layer' | 'styles.split'
content: string
/** Absolute path when written to disk; omitted for string sinks (Vite/PostCSS). */
path?: string
outfile?: string
outdir?: string
cwd?: string
manifest?: CssgenDoneManifest
layerRanges?: CssgenDoneLayerRanges
}