Skip to content

Want to skip the docs? Check out pandamastery.com - the best way to learn Panda CSS

Migration

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 exist

See 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 or cx instead.
  • 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 on strictTokens to 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

Edit this page on GitHubView as markdown
Last updated on