Update 03/19/2024: Theo - t3.gg reviewed my article and made a video about it. In turn, I replied to his review in a video and clarified a few points from the article.
Hey there!
It's the first issue of my newsletter for 2024. I wish you a Happy New Year with overcoming challenges, smashing goals, and loads of tech fun!
I've been using Panda CSS for a few weeks now to build the website and the demos of XState by Example, and I've got a lot of feelings I want to share with my fellow web developers.
For a bit of history, Tailwind CSS was my go-to styling solution for nearly five years, and no styling solution has seduced me until recently. Tailwind CSS is excellent for a few reasons:
font-size
of 14px
or 15px
.I bought Tailwind UI – the paid templates built by the Tailwind CSS team – and use it for my clients and my side projects. Without exaggerating, it's one of the best purchases I've made. The ROI is high for the price it costs.
I learned much about CSS and designing user interfaces thanks to Tailwind CSS. The reason is that the default preset gives excellent tools to non-designers people. The sole job of finding colors that look good together is difficult. The ease of prototyping and the fantastic documentation quality can explain that, too.
I started hearing about Panda CSS on Twitter less than a year ago. Alexandre Stahmer – one of the core contributors to Panda CSS – was advocating for it. I only dug deeper recently when I asked myself what I would use to style my new project, XState by Example.
Panda CSS is a CSS-in-JS solution that extracts the styles at build-time instead of runtime, resulting in better performance. It looks like this:
import { css } from "./styled-system/css";
function Example() {
return (
<div
className={css({
mt: "4",
fontSize: "xl",
fontWeight: "semibold",
bg: { base: "gray.50", _hover: "gray.200" },
lg: { fontSize: "2xl", },
})}
>
Panda CSS is the best.
</div>
);
}
And will generate the following markup:
<div class="mt_4 fs_xl font_semibold bg_gray.50 hover:bg_gray.200 lg:fs_2xl">
Panda CSS is the best.
</div>
You can group styles by conditions (lg
for large breakpoint) or by property (bg
and then base
and _hover
). Tailwind CSS does not support this yet, but it is a game changer for readability.
Panda CSS relies on codegen to generate only the styles used in the project. What I love about Panda is that styles are type-safe: you define styles by calling the css()
function, whose types only accept the styles existing in your project. The codegen also generates the type definition of this function so that it reflects Panda's configuration in the project. The codegen generates its output in a styled-system
directory at the project's root.
To me, type safety is Panda CSS's most significant advantage over Tailwind CSS. With Tailwind, developers must use a VS Code extension to check the validity of the classes they wrote in a string. Tailwind's DX (Developer Experience) is deteriorated because styles are written in a string without any native type-checking possibility.
One caveat with Tailwind CSS is merging styles. Say we have a reusable component <Button />
, and we want to override its vertical padding once. I would do:
function Button({ classNames }) {
return (
<button
classNames={"px-2 py-1 bg-gray-100" + " " + classNames}
>
{ /** ... */ }
</button>
);
}
<Button classNames="py-4" />
// Final classNames: "px-2 py-1 bg-gray-100 py-4"
The problem is that it's hard to determine if the vertical padding will be overridden. Tailwind will both generate py-1
and py-4
classes and put them in a CSS file with the same specificity, which makes the class defined last take precedence over the other one. But which one is it? I can't tell you!
There are many solutions to circumvent this, like using the !important
keyword on the overriding class or expecting the React component to receive a property verticalPadding
and letting the component choose which class to apply.
The ultimate solution is to use tailwind-merge. It finds the classes defining the same property and keeps the last.
import { twMerge } from 'tailwind-merge';
twMerge("px-2 py-1 bg-gray-100", "py-4")
// Returns: "px-2 bg-gray-100 py-4"
Panda CSS is even simpler: it's supported out-of-the-box.
import { css } from "./styled-system/css";
css({ px: "2", py: "1", bg: "gray.100" }, { py: "4" })
// Returns: "px_2 py_4 bg_gray.100"
I've often written code like that to create a badge component with variants:
type AppBadgeStatus = "pending" | "success" | "error";
interface AppBadgeProps {
status: AppBadgeStatus;
children: React.ReactNode;
}
function AppBadge({ status, children }: AppBadgeProps) {
const classesForStatus: Record<AppBadgeStatus, string> = {
success: "bg-green-100 text-green-800",
pending: "bg-yellow-100 text-yellow-800",
error: "bg-red-100 text-red-800",
};
return (
<span
className={
`inline-flex items-center font-medium ${classesForStatus[status]}`
}
>
{children}
</span>
);
}
The code becomes even more complex with more variants, like the badge size. The issue with merging styles is also relevant here.
Class Variance Authority is a more robust pattern for building component variants. A library specifically for Tailwind CSS is called Tailwind Variants. I've never used it, but I would probably if I had to create a library of components with Tailwind.
Panda CSS supports it out-of-the-box—once again:
import { styled } from "./styled-system/jsx";
import { cva, type RecipeVariantProps } from "./styled-system/css";
export const appBadge = cva({
base: {
display: "inline-flex",
alignItems: "center",
fontWeight: "medium",
},
variants: {
status: {
success: {
color: "green.800",
bg: "green.100",
},
pending: {
color: "yellow.800",
bg: "yellow.100",
},
error: {
color: "red.800",
bg: "red.100",
},
},
},
});
// Define the component
// 1.
type AppBadgeVariants = RecipeVariantProps<typeof appBadge>;
// { status: "success" | "pending" | "error" }
type AppBadgeProps = AppBadgeVariants & { children: React.ReactNode };
export function AppBadge(props: AppBadgeProps) {
return (
<span className={appBadge(props)}>
{props.children}
</span>
);
}
// 2.
export const AppBadge = styled("span", appBadge);
The variants are strongly typed automatically: you must pass the status
variant when using the appBadge
styles or the AppBadge
component, and provide one of the defined options.
It's pretty good. I like how the library has been carefully thought out as a whole.
Panda CSS is a recent library with many exciting and well-made features. Amongst all its features, I especially like the following:
cva
variants, even those never used at runtime. Using Config Recipes, Panda CSS can generate the CSS only for the actually used.vstack()
or circle()
.red.400
to style an error badge, using the error
color would make sense. Semantic Tokens allow this.<Box p="4" />
.If you want to know more about Panda CSS, Alexandre Stahmer wrote a thorough article explaining each of its features. Segun Adebayo, the creator of Chakra UI and Panda CSS, wrote about the roadmap to Panda CSS v1.
This is all for today. I hope you are intrigued by Panda CSS and will look at it!
Best,
Baptiste