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.extendwork. - Hooks for the
preset:resolvedexample this page's fix is based on. - Federated Micro-Frontends for the separate, multi-bundle version of this problem.