From Tailwind CSS to PandaCSS
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.