Skip to content

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

Customization

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

HookFiresUse it to
config:resolvedAfter presets merge, before defaults applyStrip or rewrite the resolved config
preset:resolvedWhen a preset resolves, before configs mergeFilter what a preset contributes
parser:beforeAfter a file is read, before it's parsedRewrite non-TSX source into syntax Panda can parse
codegen:prepareBefore generated files are writtenAdd or modify JS/.d.ts artifacts
codegen:doneAfter generated files are writtenReact to the finished styled-system output
cssgen:doneAfter final CSS is producedObserve 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
}
Edit this page on GitHubView as markdown
Last updated on