Skip to content

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

Customization

Plugins

Change the config, rewrite files before Panda reads them, or react to the output, from a plugin you can share.

A plugin is a named object with a hooks map. Each hook runs at one point in the build: after the config resolves, before a file is parsed, after code or CSS is written. Add the plugin to plugins and Panda calls it.

panda.config.ts

import { defineConfig, definePlugin } from '@pandacss/dev'
 
const removeStack = definePlugin({
  name: 'remove-stack',
  hooks: {
    'config:resolved': ({ config, utils }) => utils.omit(config, ['patterns.stack'])
  }
})
 
export default defineConfig({
  plugins: [removeStack]
})

definePlugin gives you autocomplete for hook names and arguments.

Six hooks are available, and they fall into three jobs:

  • Change the config before Panda uses it: config:resolved, preset:resolved.
  • Rewrite a file before Panda parses it: parser:before.
  • Touch or observe the output: codegen:prepare, codegen:done, cssgen:done.

Change the config

config:resolved runs after your presets and config merge. It receives the final config and a utils object with omit, pick, and traverse. Return a new config to replace it, or nothing to leave it alone.

definePlugin({
  name: 'no-float',
  hooks: {
    'config:resolved': ({ config, utils }) => {
      return utils.omit(config, ['patterns.float', 'utilities.float'])
    }
  }
})

This is where most one-off changes go: dropping a pattern, renaming a utility, adding a token from an environment variable.

Trim a preset

preset:resolved runs once per preset, before the presets merge. It receives the preset's config and its name. Use it to keep a third-party preset but not all of it, instead of forking it.

definePlugin({
  name: 'no-preset-colors',
  hooks: {
    'preset:resolved': ({ preset, name, utils }) => {
      if (name !== '@pandacss/preset-panda') return
      return utils.omit(preset, ['theme.tokens.colors', 'theme.semanticTokens.colors'])
    }
  }
})

Rewrite a file

parser:before receives { filePath, content } for every file Panda scans. Return a string and Panda parses that instead. This is how you feed Panda a syntax it doesn't understand: strip or rewrite the parts its parser would choke on.

definePlugin({
  name: 'strip-frontmatter',
  hooks: {
    'parser:before': ({ filePath, content }) => {
      if (!filePath.endsWith('.md')) return
      return content.replace(/^---[\s\S]*?---/, '')
    }
  }
})

Instead of checking the path yourself, scope any hook with { filter, handler }. filter.id matches the file path and filter.code matches its content. Each takes a glob, a regex, or an { include, exclude } pair.

definePlugin({
  name: 'strip-frontmatter',
  hooks: {
    'parser:before': {
      filter: { id: '**/*.md' },
      handler: ({ content }) => content.replace(/^---[\s\S]*?---/, '')
    }
  }
})

Vue, Svelte, and Astro files need none of this. The compiler reads them directly.

React to the output

Two hooks fire after Panda writes files. Both are for reporting and side effects. Whatever you return is ignored.

codegen:done runs after the styled-system folder is written, with the list of files and the outdir.

cssgen:done runs after the final CSS is produced by the CLI, the Vite plugin, or the PostCSS plugin. It receives the artifact name, the CSS content, and the path when the CSS went to disk.

definePlugin({
  name: 'css-report',
  hooks: {
    'cssgen:done': ({ artifact, content, path }) => {
      if (artifact !== 'styles.css') return
      console.log(`Generated ${content.length} bytes at ${path}`)
    }
  }
})

To shrink the CSS, don't reach for a hook. Panda's own optimizations remove unused tokens and keyframes, and anything else belongs in a PostCSS step after Panda.

One hook does let you touch generated code. codegen:prepare runs before the styled-system files are written and receives the artifacts about to be emitted. Return a modified list to add or edit files. Reach for it only when config:resolved can't express the change.

Share a plugin

A plugin is already a value, so sharing it means exporting it. Wrap it in a function when it takes options.

panda-plugin-report.ts

import { definePlugin } from '@pandacss/dev'
 
export function report(options: { verbose?: boolean } = {}) {
  return definePlugin({
    name: 'report',
    hooks: {
      'cssgen:done': ({ artifact, content }) => {
        if (options.verbose || artifact === 'styles.css') console.log(artifact, content.length)
      }
    }
  })
}

panda.config.ts

import { defineConfig } from '@pandacss/dev'
import { report } from 'panda-plugin-report'
 
export default defineConfig({
  plugins: [report({ verbose: true })]
})

Plugins run in the order they appear in plugins. Unlike a preset, a plugin can't be extended by the config that uses it. It runs as written.

Every hook's arguments and return value are listed in the plugins reference.

Edit this page on GitHubView as markdown
Last updated on