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

design systems
hooks

Panda Integration Hooks

Leveraging hooks in Panda to create custom functionality.

Panda hooks let you add new functionality or change existing behavior at specific points in the compiler lifecycle.

Hooks are callbacks you attach to named plugins in the plugins array. There is no root-level hooks object — every hook lives on a plugin with a name and a hooks map.

import { defineConfig } from '@pandacss/dev'
 
export default defineConfig({
  plugins: [
    {
      name: 'my-plugin',
      hooks: {
        'cssgen:done': ({ content, path }) => {
          // ...
        },
      },
    },
  ],
})

Here are some things you can do with hooks:

  • Modify the resolved config (config:resolved), like stripping out patterns, tokens, or keyframes.
  • Modify a preset after it's resolved (preset:resolved), like removing tokens or theme properties from a preset.
  • Transform a source file into tsx-friendly syntax before it's parsed (parser:before), so Panda can extract its style usage — this also lets you support templating languages Panda doesn't parse natively.
  • Adjust the generated JS and DTS artifacts before they're written (codegen:prepare), or react after they're written (codegen:done).
  • Observe the final CSS after it's produced (cssgen:done), for reporting or downstream tooling.

Examples

Modifying the config

Use the utils helpers on the config:resolved hook to change the resolved config. 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'])
        },
      },
    },
  ],
})

Modifying presets

Use the preset:resolved hook to change a preset after it's resolved. This is useful for filtering out parts of a preset.

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
        },
      },
    },
  ],
})

Transforming a source file before parsing

Use parser:before to rewrite a file's content before Panda parses it. The hook receives { filePath, content } and returns the transformed string. Return nothing to leave the content unchanged.

This is how you support source that isn't standard tsx — pre-process it into syntax Panda's parser understands, then return it.

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]*?---/, '')
        },
      },
    },
  ],
})

To scope a hook to specific files, pass an object with a filter and a 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,
        },
      },
    },
  ],
})

Observing the final CSS

cssgen:done runs after the final CSS is produced, for the CLI, Vite, and PostCSS. It's observe-only — the hook receives { artifact, content, path?, … } and its return value is ignored, so you can't rewrite the CSS here.

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 strip unused tokens or keyframes from the final CSS, use the top-level optimize config instead of a hook:

import { defineConfig } from '@pandacss/dev'
 
export default defineConfig({
  optimize: {
    removeUnusedTokens: true,
    removeUnusedKeyframes: true,
  },
})

For any other CSS transform, run PostCSS after Panda.

💡

Note: With removeUnusedTokens, you can't rely on the JS function token.var (or token(xxx) where xxx is a semanticToken path) from styled-system/tokens, because the CSS variables are removed based on the usage found in the generated CSS.

Sharing hooks

Hooks are shared as plugins. A plugin is a plain object with a name and a hooks object.

Plugins differ from presets in that they can't be extended, but they run in sequence in the order they appear in the plugins array, with the user's own config called last.

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],
})

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 can be a plain function or an object with a filter and a handler, which is useful for scoping parser:before to specific files:

export type PandaHook<Handler> = Handler | { filter?: HookFilter; handler: Handler }

The argument types are:

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
}