Migrating from StyleX
Migrate your project from Meta's StyleX to Panda and see how create, defineVars, and props map across.
StyleX and Panda share the same foundation: static extraction with a Rust compiler, atomic output, object syntax. The difference is scope:
- StyleX stays minimal. No globals, no built-in variants. Your team hand-rolls its conventions, and hand-rolled conventions drift.
- Panda gives you the conventions. Variants, patterns, and semantic tokens as typed APIs, enforced by the compiler and the lint rules. More surface area up front, but each piece replaces a convention you'd otherwise write and police yourself.
Value the minimalism? You don't have to take the batteries: start from
zero presets or just @pandacss/preset-base, bring your own tokens, and add
conventions back one at a time. Install through your framework guide.
Theme to tokens
stylex.defineVars (which must live in a .stylex.ts file as a named export) becomes theme.tokens in config,
referenced by name instead of imported variable objects:
vars.stylex.ts
import * as stylex from '@stylexjs/stylex'
// before
export const colors = stylex.defineVars({
brand: '#0ea5e9'
})panda.config.ts
import { defineConfig } from '@pandacss/dev'
// after
export default defineConfig({
theme: {
extend: {
tokens: {
colors: {
brand: { value: '#0ea5e9' }
}
}
}
}
})import { css } from '../styled-system/css'
<button className={css({ bg: 'brand' })} />See Tokens.
Inline styles to css()
stylex.create plus stylex.props() spread becomes css() building a class name:
import * as stylex from '@stylexjs/stylex'
// before
const styles = stylex.create({
base: { color: 'white', backgroundColor: 'blue' }
})
<div {...stylex.props(styles.base)} />import { css } from '../styled-system/css'
// after
<div className={css({ color: 'white', bg: 'blue' })} />See JSX Style Props for the styled factory and css prop forms.
Variants
StyleX's common pattern is named style objects picked by an untyped string key. Panda's recipes formalize it as a typed variant map:
// before
const styles = stylex.create({
primary: { backgroundColor: 'blue' },
secondary: { backgroundColor: 'gray' }
})
<button {...stylex.props(styles[variant])} />button.ts
// after: the prop and its allowed values are checked at compile time
import { cva } from '../styled-system/css'
export const button = cva({
variants: {
variant: {
primary: { bg: 'blue.500' },
secondary: { bg: 'gray.500' }
}
}
})See Recipes and Slot Recipes for multi-part components.
View transitions
stylex.viewTransitionClass maps one to one to Panda's viewTransition, same group/old/new keys, both
built on view-transition-class:
// before
const slide = stylex.viewTransitionClass({
group: { animationDuration: '0.4s' },
old: { opacity: 0 },
new: { opacity: 1 }
})// after
import { viewTransition } from '../styled-system/css'
const slide = viewTransition({
group: { animationDuration: '0.4s' },
old: { opacity: 0 },
new: { opacity: 1 }
})See View Transitions.
Keyframes
stylex.keyframes maps one to one to Panda's keyframes. Both take the same
from/to/<percentage> stops and return an animation name you put in
animationName or the animation shorthand:
// before
const fade = stylex.keyframes({
from: { opacity: 0 },
to: { opacity: 1 }
})
const styles = stylex.create({
box: { animationName: fade, animationDuration: '0.6s' }
})// after
import { css, keyframes } from '../styled-system/css'
const fade = keyframes({
from: { opacity: 0 },
to: { opacity: 1 }
})
const box = css({ animationName: fade, animationDuration: '0.6s' })Panda emits the @keyframes block for you and tree-shakes it to what you
reference. Shared, design-system animations belong in theme.keyframes instead,
referenced by name (animationName: 'fade'). See Keyframes.
Position try
stylex.positionTry maps one to one to Panda's positionTry. Both take the
anchor-positioning declarations and return the dashed-ident you put in
positionTryFallbacks:
// before
const flip = stylex.positionTry({
top: 'anchor(bottom)',
insetInlineStart: 'anchor(start)'
})
const styles = stylex.create({
popover: { positionTryFallbacks: flip }
})// after
import { css, positionTry } from '../styled-system/css'
const flip = positionTry({
top: 'anchor(bottom)',
insetInlineStart: 'anchor(start)'
})
const popover = css({ positionTryFallbacks: flip })Panda emits the @position-try block and tree-shakes it. Reusable fallbacks
belong in theme.positionTry, referenced by name (positionTry('flip')). See
Position Try.
Value fallbacks
stylex.firstThatWorks maps one to one to Panda's firstThatWorks. Same name, same order: the value you
want first. Both emit the declarations reversed, so the browser keeps the last one it understands:
// before
const styles = stylex.create({
header: { position: stylex.firstThatWorks('sticky', '-webkit-sticky', 'fixed') }
})// after
import { css, firstThatWorks } from '../styled-system/css'
const header = css({ position: firstThatWorks('sticky', '-webkit-sticky', 'fixed') })position: fixed;
position: -webkit-sticky;
position: sticky;Two differences. StyleX folds var() members into one nested var(a, var(b, c)); Panda keeps each member
as its own declaration, because its token variables always exist and a native var(--x, red) fallback passes
through untouched. And Panda needs at least two values, since one has nothing to fall back to. See
Value Fallbacks.
Color modes
StyleX keys a defineVars value to a media query. Panda's semantic tokens are the direct equivalent, branching on
the _dark condition:
// before
const DARK = '@media (prefers-color-scheme: dark)'
export const colors = stylex.defineVars({
bg: { default: 'white', [DARK]: 'black' }
})panda.config.ts
// after
theme: {
extend: {
semanticTokens: {
colors: {
bg: { value: { base: 'white', _dark: 'black' } }
}
}
}
}See Multiple Themes for setups beyond media-query switching (a toggled class or attribute).
Layout components
StyleX has no built-in layout primitives, consistent with its single-element scope. Panda ships patterns as both functions and JSX components:
import { Box, Stack } from '../styled-system/jsx'
<Stack gap="4">
<Box bg="gray.100">Item</Box>
</Stack>See Patterns.
Scoping: constraints become choices
StyleX forbids global styles and descendant selectors by design. Panda supports both, plus typed conditions for
the cross-element cases stylex.when gates:
Global styles, for resets and base styles:
panda.config.ts
globalCss: {
body: { margin: 0 }
}Descendant selectors, styling children from the parent:
css({ '& > li': { marginBlock: '2' } })Cross-element state, as conditions instead of a separate API:
<div className={css({ _groupHover: { opacity: 1 } })} />The specificity discipline StyleX enforced at compile time becomes your choice:
cascade layers keep precedence predictable, and the opt-in
no-descendant-selectors lint rule brings back per-element
scoping as a lint error where you want it.
Run both during the migration
Panda's CSS ships inside cascade layers and StyleX's output is unlayered, so StyleX styles win over converted components by default. See Migrating alongside unlayered CSS for the two ways to control which side wins.
After migrating
- Replace the compiler-enforced discipline. StyleX's compiler rejected what its model forbids. Panda's
lint rules are the equivalent enforcement: raw values where a token exists,
invalid token paths,
!important, physical properties. - Keep the zero-runtime output. Source transforms rewrite static calls to plain class strings, matching StyleX's compiled output model.
See also
- Cascade Layers for how Panda manages selector precedence, the nearest equivalent to StyleX's specificity guarantees.
- Writing Styles for the full
css()model this guide maps onto.