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

design systems
avoiding collisions

Avoiding CSS collisions

What happens when two presets define a token or recipe under the same name in a single build, and how to catch it.

This page is about a single Panda build that combines more than one source of tokens and recipes, your own config plus one or more presets. It's a different problem from Federated Micro-Frontends, which covers multiple separately-built bundles loaded onto the same page at runtime. If your setup is the federated case, prefix is the fix, and that guide covers it in depth. This page covers what happens earlier, before any of that CSS is even emitted.

The collision

Presets merge in order, and later entries win on conflicts. That's fine when you're intentionally overriding one preset's tokens with another's, it's the mechanism theme.extend relies on. It's a problem when it happens by accident: two presets you didn't expect to overlap both define a button recipe, or both define a brand color token, with different values.

panda.config.ts

export default defineConfig({
  presets: ['@acme/marketing-preset', '@acme/product-preset']
})

If both presets define theme.recipes.button, whichever preset is listed last wins, completely. There's no error, no warning, and no partial merge of the two recipes, one definition silently replaces the other. The same is true for a token defined in both presets, or in a preset and your own theme (not theme.extend).

Check whether you actually have one

Panda doesn't detect this for you at build time, since it can't know which name collisions are intentional overrides and which are accidents. Run panda debug and open styled-system/debug/config.json:

panda debug

Search that file for the recipe or token name you're unsure about. There's only ever one entry per name in the merged output, so if it's there at all, check whether its definition (its variants, its value) actually matches the preset you expected it to come from. If it doesn't, the other preset won. See Debugging for what else panda debug outputs.

Fix it by renaming, not by reordering

Reordering presets only changes which definition wins, it doesn't stop the collision, and it silently flips which one you're now shipping. Rename instead, ideally in the preset's own source. If you don't own the preset, do it at the boundary with a preset:resolved hook, which runs once per preset before they're merged:

panda.config.ts

import { defineConfig } from '@pandacss/dev'
 
export default defineConfig({
  presets: ['@acme/marketing-preset', '@acme/product-preset'],
  plugins: [
    {
      name: 'rename-marketing-button',
      hooks: {
        'preset:resolved': ({ preset, name }) => {
          if (name === '@acme/marketing-preset' && preset.theme?.recipes?.button) {
            preset.theme.recipes.marketingButton = preset.theme.recipes.button
            delete preset.theme.recipes.button
          }
          return preset
        }
      }
    }
  ]
})

Now both recipes exist side by side as marketingButton and productButton, instead of one silently replacing the other. Apply the same pattern to theme.tokens for a colliding token name. See Hooks for the rest of what preset:resolved can do.

This is a different mechanism from prefix, which namespaces every emitted class name and CSS variable uniformly at the config level for multi-bundle setups. Here, the collision is inside a single config's own merged data, not between two builds, so the fix is renaming the specific colliding keys, not prefixing everything.

See also

  • Presets for how preset merging and theme.extend work.
  • Hooks for the preset:resolved example this page's fix is based on.
  • Federated Micro-Frontends for the separate, multi-bundle version of this problem.