Style isolation
Load multiple Panda-built bundles on one page without class or token collisions.
Two independently built Panda bundles on the same page can emit the same class name, or the same CSS variable, with
different rules behind them. The cascade keeps one and silently drops the other. prefix and cssVarRoot stop that.
When you need this
Any setup that loads two or more independently built Panda stylesheets into one document:
- Webpack Module Federation, single-spa, qiankun, or other micro-frontend orchestrators
- Widgets injected into pages you do not control (Intercom-style embeds, browser extensions)
- Two versions of the same design system on one page, common mid-migration
If the bundles never share a document (iframes, Next.js multi-zone routing), you do not need this page. The cascade cannot cross documents.
Two kinds of collision
Override leakage
Two bundles load the same styles, and one adds a local override:
/* bundle 1 */
.ds-background {
background-color: blue;
}
.local-override {
background-color: red;
}
/* bundle 2 */
.ds-background {
background-color: blue;
}Load order decides whether .local-override wins. Fix it with a cascade layer:
unlayered styles always beat layered ones, so the override wins regardless of load order.
panda.config.ts
export default defineConfig({
layers: {
recipes: 'ds.recipes',
utilities: 'ds.utilities'
}
})Cross-version collision
Bundle A ships @acme/ds@1, bundle B ships @acme/ds@2. Both emit .button and --colors-brand, with different
values:
/* v1's bundle */
:root {
--colors-brand: #ea580c;
}
.button {
background: var(--colors-brand);
padding: 8px 12px;
}
/* v2's bundle */
:root {
--colors-brand: #2563eb;
}
.button {
background: var(--colors-brand);
padding: 20px 40px;
}Same selector, different rule. Layers do not help here: both versions emit into the same layer, and within a layer load
order still decides. This is what prefix is for.
Set a prefix
Panda prepends prefix to every emitted class name and CSS variable:
panda.config.ts
export default defineConfig({
prefix: 'acme-v1'
}):where(:root, :host) {
--acme-v1-colors-brand: #ea580c;
}
.acme-v1-button {
background: var(--acme-v1-colors-brand);
}A second bundle with prefix: 'acme-v2' emits .acme-v2-button and --acme-v2-colors-brand. Both sit in the same
<head> with nothing to fight over. Source code does not change: css(), cva(), and recipes read prefix from
config and emit the namespaced names.
Derive the prefix
A hardcoded prefix drifts the moment you forget to bump it. Key it to the package name and major version:
panda.config.ts
import { defineConfig } from '@pandacss/dev'
import pkg from './package.json' with { type: 'json' }
const major = pkg.version.split('.')[0]
const slug = pkg.name.replace(/[@/]/g, '-').replace(/^-/, '')
export default defineConfig({
prefix: `${slug}-v${major}` // @acme/ds@1.4.2 → acme-ds-v1
})A 2.x release rotates the prefix on its own. Patch and minor releases share one, which matches how you already reason about breaking changes.
Who sets the prefix
The design system does, keyed to its package and major version as above. Its components are built against its own
styled-system, so their class names are fixed when you publish.
- The library ships a static CSS file (Consume without Panda): the prefix is in the CSS the apps load.
- The apps run Panda (Consume with Panda): apps inherit the prefix
through
designSystem. Leaveprefix,hash, andseparatorunset in the app. If the app overrides them, its CSS uses names the design system's components never produce, and those components render unstyled. Panda warns withdesign_system_option_mismatch.
Two apps pinning different major versions then emit .acme-ds-v1-button and .acme-ds-v2-button, with nothing to set
in either app.
Scope tokens with cssVarRoot
prefix renames the variables. If they should also apply only inside your subtree, set cssVarRoot:
panda.config.ts
export default defineConfig({
prefix: 'acme-v1',
cssVarRoot: '.acme-v1-scope'
})<div className="acme-v1-scope">
<Button>Click</Button>
</div>Elements outside .acme-v1-scope cannot resolve the tokens. Optional: prefix alone covers most setups.
Tokens still collide
Atomic classes from raw values never collide, because the value is in the name (.bg_\#ea580c). Token references are
the trap:
const button = cva({
base: { background: 'brand' }
})
// both versions emit .bg_brand { background: var(--colors-brand) }The class names match across versions, so nothing clashes structurally. But each version declares --colors-brand with
a different value, the cascade picks one, and the losing bundle renders with the wrong brand color. prefix fixes this
too, by renaming the variable.
Verify the output
Grep the emitted CSS for the names that used to collide:
grep -E "^\.button|--colors-brand:" styled-system/styles.cssEvery match should carry your prefix. If not, check that prefix sits at the top level of defineConfig, not inside
theme, and regenerate.
What prefix does not solve
- Same-prefix collisions. Two bundles that both pick
prefix: 'design'collide as if neither had one. Choose it like a package name. - Reset collisions. Preflight targets raw element selectors, so two resets fight regardless of prefix. Set
preflight: falsein bundles that ship into a host page, and let the host own the reset. - Hand-written CSS.
prefixapplies to what Panda emits, nothing else.
For total isolation against an untrusted host, use an iframe or Shadow DOM. Heavier, but bulletproof.
See also
- Cascade Layers for single-version override leakage
- Hashing for short opaque class names instead of namespaces
- Consume with Panda and Consume without Panda for the design system setups above