Migrating from Tailwind CSS
Migrate your project from Tailwind CSS to Panda and see how utility classes map to Panda's style objects.
This guide outlines the steps needed to migrate your project from Tailwind CSS to Panda and highlights key design differences between the two.
Disclaimer: This isn't about which one is best. Both are utility-driven and both extract to static CSS at build time, the difference is mostly in syntax and in what ships built in.
Here are some similarities between the two:
- Both are utility-first: most styling happens through short property/value pairs rather than hand-written CSS classes or selectors.
- Both extract styles statically at build time by scanning your source files, there's no client-side style engine in either.
- Both support responsive and state-based variants (hover, focus, dark mode) as a first-class part of the syntax.
- Both let you define your own design tokens (colors, spacing, fonts) in a central config.
Here's where they differ.
Type safety
This is the sharpest difference between the two, and it isn't about runtime cost, both tools extract to static CSS at build time. It's about what's checked before that extraction runs.
Tailwind styles are class-name strings. Your editor can't check them, so a misspelled or unknown class is a silent
no-op, the style just doesn't apply and nothing warns you. As a project grows, that's how you get class-name soup: long
class="..." strings no one can safely refactor, variants copy-pasted across files, and dead classes that never did
anything.
Panda styles are objects. Properties are typed keys and values draw from your typed tokens, so a mistyped property is a TypeScript error at the call site, before you run a build. Autocomplete offers your real tokens and recipe variants as you type.
import { css } from '../styled-system/css'
// ❌ Tailwind: a mistyped class is a silent no-op
;<div class="bg-red-500 aligns-center" />
// ✅ Panda: a mistyped property won't type-check
;<div className={css({ bg: 'red.500', aligns: 'center' })} /> // 'aligns' does not exist
Turn on strictTokens to reject raw values too, so a color has to be a token, not a
one-off like #f00.
Theming
Tailwind v4 is CSS-first: there's no tailwind.config.js by default, tokens are defined directly in CSS with
@theme:
app.css
@import 'tailwindcss';
@theme {
--color-brand: #0ea5e9;
}If you're still on Tailwind v3, the equivalent lives in tailwind.config.js's theme key instead. Either way,
Panda's tokens live in panda.config.ts's theme key, the same idea, named token categories, but Panda's theme also
generates typed token names you get autocomplete for in css() calls:
panda.config.ts
import { defineConfig } from '@pandacss/dev'
export default defineConfig({
theme: {
extend: {
tokens: {
colors: {
brand: { value: '#0ea5e9' }
}
}
}
}
})See Tokens for the full token model, including semantic tokens, tokens whose value changes per theme or color mode, which Tailwind handles by hand through CSS variables rather than as a first-class config concept.
The sx prop, and arbitrary values
Tailwind has no dedicated escape-hatch prop, since every style is already inline as a class name. Its nearest equivalent is bracket syntax, for a one-off value outside the configured scale:
<div class="w-[327px] top-[calc(100%-4px)]"></div>
Panda doesn't need an escape hatch for this. css() is a plain object, not a class name Tailwind has to parse and
generate a matching rule for, so any valid CSS value works directly, no bracket syntax:
import { css } from '../styled-system/css'
<div className={css({ width: '327px', top: 'calc(100% - 4px)' })} />
See JSX Style Props for the other ways Panda lets you style a component inline.
Variants
Tailwind doesn't ship a built-in way to define a component's variants (a button's size/intent combinations)
itself, that typically means composing class strings by hand, or reaching for a separate package like
tailwind-variants. Panda has this built in as recipes:
button.ts
import { cva } from '../styled-system/css'
export const button = cva({
base: { borderRadius: 'md', fontWeight: 'semibold' },
variants: {
size: {
sm: { fontSize: 'sm', px: '3', py: '1.5' },
lg: { fontSize: 'lg', px: '6', py: '3' }
}
}
})<button className={button({ size: 'lg' })}>Click me</button>
The variant combinations are typed, so button({ size: 'xl' }) is a type error if xl was never defined. See
Recipes and Slot Recipes for multi-part components.
Color Modes
Tailwind's dark mode is a dark: variant prefix. In v4 it follows the OS's prefers-color-scheme automatically,
with no config, switch to manual class-based toggling with @custom-variant in CSS if you need it:
app.css
@import 'tailwindcss';
@custom-variant dark (&:where(.dark, .dark *));<div class="bg-white dark:bg-gray-900"></div>
(On Tailwind v3, this was the darkMode: 'class' | 'media' key in tailwind.config.js instead.)
Panda's equivalent is the _dark condition, paired with a semantic token so the light/dark pair is defined once and
every user of that token gets the right value automatically:
panda.config.ts
theme: {
extend: {
semanticTokens: {
colors: {
bg: { value: { base: 'white', _dark: 'gray.900' } }
}
}
}
}<div className={css({ bg: 'bg' })} />
See Theme and Multiple Themes.
Global Styles
Tailwind expects global, element-level styles written as real CSS, typically under @layer base in your main
stylesheet:
@layer base {
button {
margin: 0;
border: 0;
}
}
Panda lets you declare the same thing as a config object, globalCss in panda.config.ts, no separate CSS file
needed:
panda.config.ts
export default defineConfig({
globalCss: {
button: { margin: 0, border: 0 }
}
})See Global Styles.
Component Styles
Tailwind has no built-in layout components, it's class-name-only, layout primitives like a Box or Stack are
something you'd build yourself or pull from a separate component library (Headless UI, shadcn/ui). Panda ships
patterns for exactly this, as both a function and a JSX component, out of the box:
import { Box, Grid } from '../styled-system/jsx'
<Grid gridTemplateColumns="repeat(2, 1fr)" gap="6">
<Box bg="gray.100">Box</Box>
</Grid>
Tailwind's @apply directive (composing utility classes inside a hand-written CSS rule) also has no Panda
equivalent, since Panda doesn't generate named utility classes for you to reference from separate CSS, use a
recipe or the cx helper for the same composition instead.
Tailwind plugins, loaded with @plugin in v4 (@tailwindcss/typography, @tailwindcss/forms) don't carry over
either, check Presets for the equivalent idea, a shareable package of tokens,
recipes, and patterns.
Conclusion
Both tools solve the same problem, utility-first styling with a static, build-time extraction step. Panda's differences are mostly about typing that utility layer (typed tokens, typed recipe variants, typed semantic tokens) rather than a different mental model, which is why most of the migration above is closer to a syntax translation than a redesign.
See also
- Migration strategy for running both tools side by side during the migration.
- Writing Styles and Recipes for the two APIs this guide leans on most.