Contact us
Contact
Blog

Development

9 min read

Panda CSS: Revolutionizing CSS-In-JS Libraries

Ivan
Ivan
Web Developer

With the introduction of Next.js’ app router, writing CSS-in-JS in Next.js apps poses a challenge. Popular CSS-in-JS libraries, like Emotion or Styled Components, currently don’t support React Server Components (RSC). Searching for a solution similar to Emotion, I came across Panda, a new type-safe CSS-in-JS library with zero runtime, multi-variant support, and RSC compatibility. It integrates many beneficial features from other CSS libraries, wrapping them into a versatile package with numerous options and features. To me, it represents an evolution of CSS-in-JS libraries. But instead of just talking about it, let me give you a deeper dive into Panda.

### What is Panda, exactly?

As mentioned earlier, Panda represents an evolution in the CSS-in-JS paradigm. I think it’s almost inaccurate to label it a CSS-in-JS library, given its lack of one of the key characteristics of such libraries: runtime compilation.

Unlike Emotion, Panda creates CSS files for the app during its build time. This is in utter contrast to Emotion, which creates styles during runtime in the browser. While this approach has benefits, it can also negatively impact performance and JS bundle size. Runtime compilation allows you to define your styles and alter them based on the state of your app and components.

Sadly, Panda doesn't support this, at least not as smoothly as other libraries. However, it's important to remember that everything involves trade-offs, and Panda is no exception.

Despite not supporting app-state-prop-style changes, Panda offers numerous other features and benefits that make up for its limitations. Let's now briefly examine some of these features to understand why Panda is worth exploring.

### JIT Compilation and Utility Classes

Panda draws inspiration from numerous other CSS libraries. A standout feature is its Just In Time (JIT) compilation, similar to Tailwind, which compiles only styles you are using and compiles  them down into utility classes.

This offers a significant advantage: any repeated styles in your app won't bloat your stylesheet since they already exist and don't need to be created again. Like Tailwind, Panda only generates the styles you use (with some exceptions).

For instance, if you define a style for a `div` as follows:

```tsx
<div className={css({
 backgroundColor: 'gray.500',
 color: 'white',
 padding: '8',
 fontSize: 'lg',
})}>
 Content in a Div
</div>

```

Here’s what the resulting HTML looks like:

```tsx
<div class="bg_gray.500 text_white p_8 fs_lg">Content in a Div</div>

```

And this will be added to the CSS file:

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

.text_white {
color: var(--colors-white);
}

.p_8 {
   padding: var(--spacing-8);
}

.fs_lg {
   font-size: var(--font-sizes-lg);
}

```

Later on, if you wish to use `padding: '8'` on another component or element, the style won’t be re-added to the stylesheet since it already exists. That way, your stylesheet won’t expand unnecessarily.

Not only does Panda adopt this great functionality from Tailwind, but it also enhances it. Your JSX/HTML code won't be cluttered with utility classes; they will only appear in the generated code. This addresses a common complaint about Tailwind, and Panda seems to have found the solution!

However, looking at the code above, you might argue that this will clutter the HTML more than Tailwind and make the code difficult to read. Don’t worry – there are alternative ways of writing CSS in Panda. Let me introduce you to some.

### Writing Styles

By assigning styles to a variable, you can make the code mentioned above look like this:

```tsx
const styles = css({
 backgroundColor: 'gray.500',
 color: 'white',
 padding: '8',
 fontSize: 'lg',
})

```

Then, you simply call the `styles` variable in the div, like this:

```tsx
<div className={styles}>Content in a Div</div>

```

At COBE, we place the styles in a separate file called `ComponentName.style.ts`, define the "root" style object, and nest styling for other elements that are part of that component.

Consider a simple Card component which looks like this:

```tsx
const Card = () => {
 return (
   <div>
     <div className="card-image">
       <img src="<https://source-to-image>" alt="Image alt" />
     </div>
     <div className="card-body">
       <h3 className="card-title">Hello from COBE</h3>
       <p className="card-text">
         Welcome to COBE - Creators Of Beautiful Experiences
       </p>
     </div>
   </div>
 );
};

export default Card;

```

Typically, we would add `styles` to the first element, and then, for those elements inside the root, we’d use class names to style them. It would look something like this:

```tsx
import {css} from "@/styled-system/css";

const styles = css({
 maxW: 'sm',
 mt: '8',
 bg: 'gray.100',
 p: '4',

 '& .card-title': {
   fontSize: '1xl',
   fontWeight: 'bold',
   mb: '2',
 },

 '& .card-text': {
   fontSize: 'sm',
   mb: '4',
 },
})

export default styles;
```

Afterward, you can simply add the exported `styles` variable to your "root" element:

```tsx
import styles from "@/app/components/Card/Card.style";

const Card = () => {
 return (
   <div className={styles}>
   ...
   </div>
 )
}

```

The JSX code is now clean and devoid of extra styles, all while retaining the benefits of utility classes.

### Shorthand Properties

To fast-track your coding, for some CSS properties, you can use shorthands. Note that the CSS I wrote uses shorthand terms like `mb` (`margin-bottom`), `bg` (`background`), `maxW` (`max-width`), but you can find the full list of shorthands [here](https://panda-css.com/docs/utilities/background). They’re pretty intuitive, and with Panda being fully typed, you’ll immediately receive suggestions in your editor.

Another neat feature is the way you can define pseudo props. Try this:

```tsx
maxW: 'sm',
mt: '8',
bg: 'gray.100',
p: '4',

'&:hover': {
bg: 'gray.100',
}
```

Panda offers an even simpler syntax:

```tsx
_hover: {
bg: 'gray.100'
}
```

You can find a [list of all supported pseudo props here](https://panda-css.com/docs/concepts/conditional-styles#reference).

### Template Literal Syntax

If you're not a fan of the object literal syntax, you can opt-in to write a template literal syntax, which allows you to write your CSS properties like normal CSS. Basically, something like this:

```tsx
const styles = css`
max-width: {sm},
margin-top: {8},
background: {gray.100},
padding: {4}
`
```

Note how I'm using predefined tokens in this code with curly braces. This is how you can access them when using template literals.

To enable template literals, you need to turn them on in the `panda.config.ts` file.

```tsx
export default defineConfig({
...
syntax: 'template-literal'
})

```

Although it's great for writing standard CSS styles, I recommend sticking with object literals for improved code completion. Note that there are some [caveats](https://panda-css.com/docs/concepts/template-literals#caveats) to be aware of when using template literals.

### "Styled Components"

Panda CSS offers another style writing method, which anyone familiar with Emotion or Styled Components will find quite intuitive.

For instance, with Emotion, you would typically write your styles like this:

```tsx
import styled from '@emotion/styled'

const CardStyled = styled.div(({ theme }) => {
   return {
     maxWidth: 500,
     background: theme.colors.white,
     ...
   }
})
```

You can then use the `CardStyled` component within your JSX. Panda offers a similar feature, with a slight difference in syntax. For our `Card` component, we could write our styles as follows:

```tsx
import { styled } from '@/styled-system/jsx'

const CardStyled = styled('div', {
 base: {
   maxW: 'sm',
   mt: '8',
   bg: 'gray.100',
   p: '4',

   '& .card-title': {
     fontSize: '1xl',
     fontWeight: 'bold',
     mb: '2',
   },

   '& .card-text': {
     fontSize: 'sm',
     mb: '4',
   },
 }
})

export default CardStyled;
```

Instead of writing the following in your component:

```tsx
<div className={styles}> ... </div>

```

You can go with:

```tsx
import CardStyled from "@/app/components/Card/Card.style";

const Card = () => {
 return (
   <CardStyled>
     <div className="card-image">
       <img src="<https://source-to-image>" alt="Image alt" />
     </div>
     <div className="card-body">
       <h3 className="card-title">Hello from COBE</h3>
       <p className="card-text">
         Welcome to COBE - Creators Of Beautiful Experiences
       </p>
     </div>
   </CardStyled>
 );
};

export default Card;
```

To enable this feature, you need to set `jsxFramework: "react"` in the `panda.config.ts` file.

Notice the `base` object in the Panda example. We need to write it like that because Panda allows you to have variants of your styles and this is the `base` variant.

### Variants... Well, Actually, Recipes

Panda features something called Recipes, which are essentially style variants. In the previous example, we defined a property of a Recipe called `base`. However, there are three other properties: `variants`, `compoundVariants`, and `defaultVariants`.

Variants provide flexibility in presenting your components. To add a variant to your `Card` component, you can include a `variants` object and add props that you can then change on the component itself. For instance:

```tsx
import { styled } from '@/styled-system/jsx'

const CardStyled = styled('div', {
 base: {
   ...
 },
 variants: {
   background: {
     danger: {
       bg: 'red.500',
     },
     warning: {
       bg: 'yellow.500',
     },
   },
   title: {
     small: {
       '& .card-title': {
         fontSize: 'sm',
       },
     },
     large: {
       '& .card-title': {
         fontSize: '2xl',
       },
     },
   }
 }})

export default CardStyled;
```

Now, you have `background` and `title` variants for your card. To display a card with a red background and a large title, write:

```tsx
<CardStyled background="warning" title="large"> ... </CardStyled>

```

If you have many variants and want to set some of them as default, just use the `defaultVariants` object. And if you wanted your card to display a red background and a large title by default, define it as a `defaultVariant`:

```tsx
import { styled } from '@/styled-system/jsx'

const CardStyled = styled('div', {
 base: {
   ...
 },
 variants: {
   ...
 },
 defaultVariants: {
   background: 'danger',
   title: 'large',
 }
})

export default CardStyled;
```

Then, you can remove the `background` and `title` prop from the `CardStyled` component.

Compound variants are used for adding additional styles to your components when specific conditions are met. For example, let's say that you want to add a border to your card when the `background` is "warning" and the `title` is "small".

```tsx
import { styled } from '@/styled-system/jsx'

const CardStyled = styled('div', {
 base: {
   ...
 },
 variants: {
   ...
 },
 defaultVariants: {
   ...
 },
 compoundVariants: [
   {
 background: 'warning',
     title: 'small',
     css: {
       border: '2px solid black'
     },
   },
 ]
})

export default CardStyled;
```

There's a lot more to explore about [Recipes](https://panda-css.com/docs/concepts/recipes) and [Slot Recipes in Panda](https://panda-css.com/docs/concepts/slot-recipes). For a more in-depth understanding, make sure to review the documentation.

### Responsive Design

One more feature worth discussing is responsive design. Panda handles this aspect really well. For instance, if you want to have different `fontSize` values for your `base`, `medium`, and `large` breakpoints, Panda simplifies this process. Make sure to just define it like this:

```tsx
fontSize: {base: 12, md: 14, lg: 16}
```

That's it. You simply define an object with breakpoint properties and add your values.

You can even use something like `mdToXl`, which applies a specific style value for `md`, `lg`, and `xl` breakpoints.

If you want to apply styling to just one breakpoint, you can use `mdOnly` which applies a specific style to the `md` breakpoint only, so there’s usually no need to write standard media queries, although that’s always an option if you need it.

### Other features

While this article touches on some of Panda's features, there's much more to discover:

- Cascade Layers
- Merging styles
- Conditional styles
- Patterns
- Theming
- Utilities
- Panda CLI

For even more insights, I recommend exploring [Panda’s documentation](https://panda-css.com/docs/overview/getting-started).

I know that I mainly discuss using Panda in Next.js with an app router in this article; it's important to note that **Panda is compatible with virtually all popular JS frameworks and build systems**, ranging from Astro to Storybook. It's not exclusively designed for React or Next.js.

### Why I don't consider Panda a "real" CSS-in-JS solution?

At the beginning of the article, I said that I don't view Panda as a typical CSS-in-JS solution, and now I'd like to elaborate on that.

Firstly, Panda doesn’t operate during runtime, a feature that distinguishes most CSS-in-JS frameworks. While enabling your CSS framework to do most of its work during runtime comes with drawbacks, like JavaScript bundle size and potential performance issues, it also brings a significant benefit – conditional styles that can depend on the component or even the app state. Unfortunately, Panda handles this aspect somewhat poorly due to its build-time CSS compilation.

For example, in Emotion, I can do something like this:

```tsx
import styled from '@emotion/styled'

const CardStyled = styled.div(({ theme, isDark }) => {
   return {
     maxWidth: 500,
     background: isDark ? theme.colors.black : theme.colors.white,
     ...
   }
})
```

Based on the parent component's state, I can simply pass an `isDark` prop to the styled component and adjust the `Card`'s background. The `isDark` prop could be hardcoded, come from a component state, global state, or a context provider – it will always work.

Unfortunately, Panda doesn't even offer the option to pass this prop. You would have to create an `isDark` variant for that and define the styling for it. I know this is not such a big deal, but Emotion’s way is a bit more readable and JavaScripty.

Another feature Panda lacks is easily defining custom transform functions, as illustrated in this example:

```tsx
import styled from '@emotion/styled'
import toRem from 'utilities/toRem'

const CardStyled = styled.div(({ theme, isDark }) => {
   return {
     maxWidth: 500,
     fontSize: toRem(20)
     ...
   }
})
```

In this case, the `toRem` function just converts pixel values into rem values, which comes in handy when translating pixel values from design tools like Figma. Unfortunately, Panda doesn't support this function. You could, however, create your own utility for this, but it wouldn’t work as seamlessly as you might want it to. For example, you’d have to define all CSS properties that the function could be used with.

While there are [workarounds](https://panda-css.com/docs/guides/dynamic-styling) to these issues, they don't work as efficiently as other CSS-in-JS solutions and often involve pre-rendering every possible style variation. Consider this when opting for Panda as your CSS framework, as there are distinct trade-offs if you’re used to other CSS-in-JS frameworks.

All things considered, the numerous features Panda offers make these trade-offs worthwhile. Plus, if you correctly set up your system with tokens, themes, utilities, and variants, you might not even encounter these problems.

### Ecosystem

As you may know, Panda was created by the same team behind the popular Chakra UI framework. While there were rumors that it may adopt Panda in the future, this isn’t [happening just yet](https://www.adebayosegun.com/blog/chakra-panda-ark-whats-the-plan#will-panda-be-used-in-chakra-ui). Chakra UI v3 will continue using Emotion as its runtime engine but will align its styling APIs with Panda, making any future migration easier.

At the moment, the recommended way to use Panda, especially if you want to use prebuilt components, is to pair it with [Ark UI](https://ark-ui.com/). Ark UI is a headless UI library similar to Radix UI Primitives or Headless UI by the Tailwind team. While Ark UI is designed to work with Panda, it also supports Tailwind or vanilla CSS. From my standpoint, this is ideal. Headless components usually provide accessibility without imposing any additional visual styles, allowing you to style the components as you wish.

If you’re comfortable styling the components on your own, you can use Panda with Ark - at least until Chakra UI adopts Panda sometime in the future.

### Conclusion

Panda presents a unique approach to handling CSS, offering various features that can simplify your work. While it has some limitations compared to other CSS-in-JS solutions, especially in handling dynamic conditional styles, its benefits make the trade-offs well worth it.

Therefore, if you’re looking for a new styling system, I cannot recommend Panda enough. Give it a try, and let me know how it went.

Like what you just read?

Feel free to spread the news!

About the author

Ivan is a Front-end developer at COBE. When he's not working or playing Soulsborne games, he's making video tutorials for YouTube.

Ivan

Web Developer

Write
Ivan
Write COBE
Related Articles

Still interested? Take a look at the following articles.