Migrating from Tailwind CSS
Migrate your project from Tailwind CSS to Panda and see how utility classes map to Panda's style objects.
Tailwind and Panda are both utility-first with build-time extraction, so migrating is closer to a syntax translation than a redesign:
// before
<div class="bg-red-500 px-4 hover:bg-red-600" />
// after
<div className={css({ bg: 'red.500', px: '4', _hover: { bg: 'red.600' } })} />Same utilities, now typed and checked by your editor. No more class soup, and no tailwind-merge: style objects
merge natively with css(base, overrides).
Utilities become style objects
Tailwind styles are class-name strings. A misspelled class is a silent no-op: the style doesn't apply and nothing warns you. Panda styles are objects with typed keys and token-typed values, so the same mistake is a TypeScript error at the call site:
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 existSee Writing Styles.
Theme to tokens
Tailwind v4 defines tokens in CSS with @theme (v3: the theme key in tailwind.config.js):
app.css
@import 'tailwindcss';
@theme {
--color-brand: #0ea5e9;
}Panda's tokens live in panda.config.ts and generate typed names you get autocomplete for:
panda.config.ts
import { defineConfig } from '@pandacss/dev'
export default defineConfig({
theme: {
extend: {
tokens: {
colors: {
brand: { value: '#0ea5e9' }
}
}
}
}
})See Tokens, including semantic tokens, which Tailwind approximates by hand with CSS variables.
Arbitrary values
Tailwind's bracket syntax covers one-off values outside the scale:
<div class="w-[327px] top-[calc(100%-4px)]"></div>css() takes any valid CSS value directly, no brackets:
<div className={css({ width: '327px', top: 'calc(100% - 4px)' })} />Tailwind's opacity shorthand carries over too: bg-red-500/40 is bg: 'red.500/40'. See
Color opacity modifier.
Variants
Tailwind has no built-in way to define a component's variants; that usually means hand-composed class strings or a
package like tailwind-variants. Panda ships it 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>Variants are typed: button({ size: 'xl' }) is a type error if xl was never defined. Multi-part components use
Slot Recipes.
Dark mode
Tailwind's dark: prefix (v4 follows prefers-color-scheme by default; @custom-variant switches to class-based
toggling):
<div class="bg-white dark:bg-gray-900"></div>Panda's _dark condition is the direct equivalent:
<div className={css({ bg: { base: 'white', _dark: 'gray.900' } })} />For a pair that repeats across the app, define it once as a semantic token and use the token everywhere:
panda.config.ts
theme: {
extend: {
semanticTokens: {
colors: {
bg: { value: { base: 'white', _dark: 'gray.900' } }
}
}
}
}<div className={css({ bg: 'bg' })} />See Conditional Styles and Multiple Themes.
Custom variants
Tailwind's @custom-variant defines your own prefix. Panda's equivalent is a condition in config:
app.css
/* before */
@custom-variant hocus (&:hover, &:focus);panda.config.ts
// after
export default defineConfig({
conditions: {
extend: {
hocus: '&:is(:hover, :focus)'
}
}
})// before
<div class="hocus:bg-red-600" />
// after
<div className={css({ _hocus: { bg: 'red.600' } })} />See Conditions.
Custom utilities
Tailwind's @utility (v3: addUtilities in a plugin) defines your own class. Panda's equivalent is a typed
utility in config:
app.css
/* before */
@utility tab-* {
tab-size: --value(integer);
}panda.config.ts
// after
export default defineConfig({
utilities: {
extend: {
tab: {
className: 'tab',
transform(value) {
return { tabSize: value }
}
}
}
}
})// before
<pre class="tab-4" />
// after
<pre className={css({ tab: 4 })} />A utility can also pull its values from a token category (values: 'spacing'), which makes them typed and
autocompleted. See Utilities.
Global styles
Tailwind global styles are real CSS under @layer base:
@layer base {
button {
margin: 0;
border: 0;
}
}Panda declares the same thing in config:
panda.config.ts
export default defineConfig({
globalCss: {
button: { margin: 0, border: 0 }
}
})See Global Styles.
Layout components
Tailwind is class-name-only; a Box or Stack comes from your own code or a component library. Panda ships
patterns as both functions and JSX components:
import { Box, Grid } from '../styled-system/jsx'
<Grid gridTemplateColumns="repeat(2, 1fr)" gap="6">
<Box bg="gray.100">Box</Box>
</Grid>Typography plugin
@tailwindcss/typography's prose classes map to @pandacss/preset-typography, a preset with a size-aware
prose recipe:
panda.config.ts
import typographyPreset from '@pandacss/preset-typography'
export default defineConfig({
presets: ['@pandacss/preset-panda', typographyPreset()]
})// before
<article class="prose prose-lg dark:prose-invert" />
// after
import { prose } from '../styled-system/recipes'
<article className={prose({ size: 'lg' })} />Dark mode needs no extra class, because the recipe's colors are semantic tokens. See Typography.
What has no equivalent
@apply. Panda doesn't generate named utility classes for separate CSS to reference. Compose with a recipe orcxinstead.- Other Tailwind plugins (
@tailwindcss/forms). The equivalent idea is a preset: a shareable package of tokens, recipes, and patterns.
Run both during the migration
Panda's CSS ships inside cascade layers and most Tailwind setups are effectively unlayered, so legacy styles win over converted components by default. See Migrating alongside unlayered CSS for the two ways to control which side wins.
After migrating
- Enforce the discipline Tailwind's fixed class set gave you. The lint rules flag
raw values where a token exists (
prefer-token) and know your actual theme. Turn onstrictTokensto reject raw values at the type level. - Drop the styling runtime. Source transforms rewrite static
css()calls to plain class strings at build time, matching Tailwind's zero-runtime output byte for byte.
See also
- Writing Styles and Recipes for the two APIs this guide leans on most.
- Utilities to build your own typed version of Tailwind's utility layer.