← Blog

From Tailwind CSS to PandaCSS

August 8, 2023 7 min read

I’m a long time user of Tailwind CSS and Tailwind CSS is awesome.

Now, I’ve been using PandaCSS to build the website. After spending a few weeks building the website using Nuxt.js and Next.js with PandaCSS, here are some reasons I like it and want to move from Tailwind CSS to PandaCSS.

Atomic Styles

Such as Tailwind CSS, PandaCSS compiled outputs your styles as atomic CSS but it’s lightweight.

 <script setup lang="ts">
import { css } from 'styled-system/css'

const styles = css({
  backgroundColor: 'gray.500',
  borderRadius: '9999px',
  fontSize: '16px',
  padding: '6px 12px'
})
</script>

<template>
  <!-- Generated class: bg_gray.500 rounded_9999px fs_16px p_6px_12px -->
  <div :class="styles">
    <p>Hello World</p>
  </div>
</template>
 

The styles generated at build time end up like this:

 @layer utilities {
  .rounded_9999px {
    border-radius: 9999px;
  }

  .p_6px_12px {
    padding: 6px 12px;
  }

  .bg_gray\.500 {
    background-color: var(--colors-gray-500);
  }

  .fs_16px {
    font-size: 16px;
  }
}
 

Shorthand Properties

PandaCSS provides shorthands for common css properties to help improve the speed of development and reduce the visual density of your styles declaration.

Properties like background, backgroundColor, borderRadius, padding, margin… can be swapped to their shorthand.

 import { css } from "styled-system/css";

// Normal CSS Properties
const styles = css({
  backgroundColor: "gray.500",
  borderRadius: "9999px",
  padding: "6px 12px",
  margin: "3px 6px",
  fontSize: "16px",
});

// Shorthand CSS Properties
const styles = css({
  bg: "gray.500",
  rounded: "full",
  p: "6px 12px",
  m: "3px 6px",
  fontSize: "16px",
});
 

Styles Groups

PandaCSS is flexible and you can group styles in many different ways.

Group by style property

 import { css } from "styled-system/css";

const styles = css({
  bg: {
    base: "red.500",
    _hover: "green.500",
    _focus: "blue.500",
  },
});
 

Group by conditional styles

 import { css } from "styled-system/css";

const styles = css({
  bg: "red.500",
  textStyle: "xs",
  _hover: {
    bg: "green.500",
    textStyle: "sm",
  },
  _focus: {
    bg: "blue.500",
    textStyle: "md",
  },
});
 

Responsive Design

Responsive design is a fundamental aspect of modern web development, allowing websites and applications to adapt seamlessly to different screen sizes and devices.

PandaCSS provides a comprehensive set of responsive utilities and features to facilitate the creation of responsive layouts. It lets you do this through conditional styles for different breakpoints.

 <script setup lang="ts">
import { css } from 'styled-system/css'
</script>

<template>
  <div :class="css({
    bg: { base: 'red.100', md: 'red.200', lg: 'red.300' }
    fontSize: { base: 'sm', md: 'md', lg: 'lg' },
    fontWeight: { base: 'normal', md: 'medium', lg: 'semibold' },
  })">
    <p>Text</p>
  </div>
</template>
 

JSX Style Props

I work with Vue.js or Nuxt almost exclusively. However, when working with React.js or Next.js, I can use PandaCSS styles as JSX components.

 import { styled } from "styled-system/jsx";

const Button = ({ children }) => (
  <styled.button bg="blue.500" color="white" py="2" px="4" rounded="md">
    {children}
  </styled.button>
);
 

Semantic Tokens

In PandaCSS have Core tokens (like colors, spacings, etc.) and Semantic tokens. Semantic tokens are meta token that reference Core tokens and have conditions like for the theme. For example, if you want a color background or text to change automatically based on light or dark mode.

 import { defineConfig } from "@pandacss/dev";

export default defineConfig({
  theme: {
    semanticTokens: {
      colors: {
        ui: {
          background: {
            DEFAULT: {
              value: { base: "{colors.white}", _dark: "${color.gray.950}" },
            },
            acent: {
              value: { base: "{colors.white}", _dark: "${color.gray.900}" },
            },
          },
          text: {
            DEFAULT: {
              value: {
                base: "{colors.slate.900}",
                _dark: "${color.slate.300}",
              },
            },
            acent: {
              value: {
                base: "{colors.slate.800}",
                _dark: "${color.slate.400}",
              },
            },
          },
        },
      },
    },
  },
});
 

Then you can use the semantic tokens by following ways.

 <script setup lang="ts">
import { css } from 'styled-system/css'
</script>

<template>
  <table>
    <thead>
      <tr>
        <th :class="css({ bg: 'ui.background' })">Name</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td :class="css({ color: 'ui.text' })">PandaCSS</td>
      </tr>
    </tbody>
  </table>
</template>
 

Patterns

PandaCSS comes with a lot of utility function to solve common layout such as box, container, stack, hstack, vstack, wrap, aspecRatio, flex, center, linkOverlay, float, grid, gridItem, divider, circle, square, visuallyHidden, bleed, cq.

 <script setup lang="ts">
import { hstack, grid } from 'styled-system/patterns'
</script>

<template>
  <div>
    <div :class="hstack({ gap: 6 })">
      <div>First</div>
      <div>Second</div>
      <div>Third</div>
    </div>
    <div :class="grid({ columns: 3, gap: 6 })">
      <div>First</div>
      <div>Second</div>
      <div>Third</div>
    </div>
  </div>
</template>
 

PandaCSS also supports JSX components for patterns.

 import { Stack, Circle } from "styled-system/jsx";

const BadgeButton = ({ children }) => (
  <Stack gap="4" align="center">
    <button>{children}</button>
    <CIrcle size="4">4</Circle>
  </Stack>
);
 

Recipes and Slot Recipes

PandaCSS provides a way to write CSS-in-JS with better performance, developer experience, and composability.

Recipes

Recipes come in handy when you need to apply style variations to one part of a component such as button, label, alert… Recipes in PandaCSS can be used with Atomic Recipe (or cva) or Config Recipe.

Atomic Recipe (or cva)

Atomic recipes are a way to create multi-variant atomic styles with a type-safe runtime API. They are defined using the cva function which was inspired by Class Variance Authority. The cva function which takes an object as its argument.

 import { cva } from 'styled-system/css'

export const label = cva({
 base: {
  color: 'gray.700',
  fontWeight: '500',
 },
 variants: {
  size: {
   xs: {
    textStyle: 'xs',
   },
   sm: {
    textStyle: 'sm',
   },
   md: {
    textStyle: 'md',
   },

   lg: {
    textStyle: 'lg',
   },
  },
 },
 defaultVariants: {
  size: 'md',
 },
})
 

Config Recipe

Config recipes are extracted and generated just in time, this means regardless of the number of recipes in the config, only the recipes and variants you use will exist in the generated CSS.

 import { defineRecipe } from '@pandacss/dev'

export const label = defineRecipe({
 className: 'label',
  description: 'The styles for the label component',
 base: {
  color: 'gray.700',
  fontWeight: '500',
 },
 variants: {
  size: {
   xs: {
    textStyle: 'xs',
   },
   sm: {
    textStyle: 'sm',
   },
   md: {
    textStyle: 'md',
   },

   lg: {
    textStyle: 'lg',
   },
  },
 },
 defaultVariants: {
  size: 'md',
 },
})
 

Slot Recipes

Slot Recipes are a better fir for more complex cases, when you need apply variations to multiple parts of the component such as accordion, combobox, dialog

Slot Recipes in PandaCSS can be used with Atomic Slot Recipe (or sva) or Config Slot Recipe.

Atomic Slot Recipe (or sva)

The sva function is a shorthand for creating a slot recipe with atomic variants. It takes the same arguments as cva but returns a slot recipe instead.

 import { sva } from 'styled-system/css'

export const slider = sva({
 slots: ['root', 'label', 'control', 'track', 'range', 'thumb', 'output'],
 base: {
  root: { width: 'full' },
  label: { color: 'gray.900', fontWeight: 'semibold' },
  control: { position: 'relative', display: 'flex', alignItems: 'center' },
  track: { bg: 'gray.200', borderRadius: 'xl', flex: '1' },
  range: { bg: 'gray.900', borderRadius: 'xl' },
  thumb: {
   bg: 'gray.900',
   borderColor: 'gray.200',
   borderRadius: 'full',
   borderWidth: '2px',
   boxShadow: 'sm',
   outline: 'none',
  },
 },
 variants: {
  size: {
   sm: {
    control: { py: 2 },
    track: { h: 2 },
    range: { h: 2 },
    thumb: { w: 6, h: 6 },
   },
  },
 },
 defaultVariants: {
  size: 'sm',
 },
})
 

Config Slot Recipe

Config slot recipes are very similar atomic recipes except that they use well-defined classNames and store the styles in the recipes cascade layer.

 import { defineSlotRecipe } from '@pandacss/dev'

export const slider = defineSlotRecipe({
 className: 'slider',
  description: 'The styles for the slider component',
 slots: ['root', 'label', 'control', 'track', 'range', 'thumb', 'output'],
 base: {
  root: { width: 'full' },
  label: { color: 'gray.900', fontWeight: 'semibold' },
  control: { position: 'relative', display: 'flex', alignItems: 'center' },
  track: { bg: 'gray.200', borderRadius: 'xl', flex: '1' },
  range: { bg: 'gray.900', borderRadius: 'xl' },
  thumb: {
   bg: 'gray.900',
   borderColor: 'gray.200',
   borderRadius: 'full',
   borderWidth: '2px',
   boxShadow: 'sm',
   outline: 'none',
  },
 },
 variants: {
  size: {
   sm: {
    control: { py: 2 },
    track: { h: 2 },
    range: { h: 2 },
    thumb: { w: 6, h: 6 },
   },
  },
 },
 defaultVariants: {
  size: 'sm',
 },
})
 

Compatible

PandaCSS is compatible with virtually all popular JavaScript frameworks such as Vue.js, React.js, Nuxt.js, Next.js, Svelte.js, Astro.js, SvelteKit, Preact, Solid.js, Remix, and more. Also, PandaCSS is compatible with all build systems, ranging from Vite to Storybook.

Conclusion

PandaCSS is a new approach to styling web apps. It’s powerful and flexible. I recommend PandaCSS if you want to build a fast, modern and performant web app.