Styling React 2023 edition
My approach for using PostCSS, Tailwind, cva and a few others tools to style react apps and components
Over the past few years, I’ve worked with React apps utilising various CSS-in-JS libraries, starting with styled-components, transitioning through emotion, Theme UI, and finally Stitches. I’ve also integrated MUI, Mantine, and Chakra in numerous client projects.
While I didn’t always opt for CSS-in-JS, I frequently chose it for its ubiquity — everyone was using it, and I was aboard the hype train 🚂. I knew there was an performance overhead when compared to vanilla CSS however, I was oblivious to how detrimental its impact on performance was until I read Sam Magura’s article, “Why We’re Breaking Up with CSS-in-JS”. This piece prompted me to search for a new standard in styling React apps. I believe I’ve found it, not in a single tool, but in a harmonious blend of various utilities.
TL;DR these are the tools I use to style React apps:
- PostCSS (or Sass)
- Tailwind
- CVA (class-variance-authority)
- Utopia
- Open Props
- clsx & tailwind-merge
Tools
PostCSS
I use PostCSS to extend CSS’s features and to add a few things that make writing styles a little more convenient, but it could easily be swapped for another preprocessor like Sass or vanilla CSS. It’s up to you. You can view my PostCSS config here.
Typically styles are colocated with the components or pages that they concern, but I also include a base line of styles that are used across the app. I use a global.css
file to import these files, which is then imported into the main app entrypoint.
Tailwind
I find Tailwind useful when used sparingly, it is perfect for allowing me to append certain styles to an element on an ad-hoc basis. For example, I like to use it to add classes to elements to add some margin or to make a flex container when I don’t want to make a new bespoke class declaration. I got the idea from Andy Bell’s “Be the browser’s mentor, not its micromanager” talk from All Day Hey 2022.
You can view my Tailwind config here, it is a little different to the default config, I have extended the spacing and font-size scales, added a few extra colours and some other bits and bobs.
CVA
CVA or class-variance-authority is a excellent package that makes it easy to create style variants for components. I do not use CVA for all components, only the ones that need different variants, e.g. buttons and other UI primitives.
Utopia
Utopia is not a product, a plugin, or a framework. It’s a memorable/pretentious word we use to refer to a way of thinking about fluid responsive design.
Using the various tools on Utopia’s website, you can copy the CSS custom properties that are output and add them to your styles. These properties are responsive, which means the values scale based on the viewport size. Follow this link for an example font-size scale.
Open Props
Open Props adds to the set by providing extra custom properties for things like easing functions or animations.
clsx and tailwind-merge
clsx is a tiny utility for constructing className
strings conditionally, I use it in conjunction with tailwind-merge which merges Tailwind CSS classes without style conflicts.
I use a cn()
function that was ripped from shadcn/ui, it is a simple utility that conditionally joins classnames together.
import { type ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
Examples
1. Basic component
For a simple component, I import a CSS file (TheComponent.css
) into the component file (TheComponent.jsx
).
// TheComponent.css
.component-wrapper {
background-color: var(--color-primary);
}
// TheComponent.jsx
import './TheComponent.css';
export const TheComponent = ({ children }) => {
return <div className="component-wrapper">{children}</div>;
};
2. Component without variants
In this slightly more complex example the cn
function is used to merge a specific class with the classes passed to the component.
import { ReactNode, ComponentPropsWithoutRef } from 'react';
import { cn } from '@/src/utils/classnames';
import './TheComponent.css';
interface TheComponentProps extends ComponentPropsWithoutRef<'div'> {
children?: ReactNode;
isActive?: boolean;
}
export const TheComponent = ({
className,
children,
isActive,
...rest
}: TheComponentProps) => {
const thecomponentClass = cn(
className,
{ active: isActive },
'component-wrapper',
);
return (
<div className={thecomponentClass} {...rest}>
{children}
</div>
);
};
3. Component with variants
As mentioned above, when using variants, I use CVA. The way that the Props
TypeScript types are setup ensure that the cva
variants are included in the component’s props.
import { cn } from '@/src/utils/classnames';
import { VariantProps, cva } from 'class-variance-authority';
import { ComponentPropsWithoutRef, ReactNode } from 'react';
import './Button.css';
export const buttonVariants = cva('button-base', {
variants: {
variant: {
primary: 'button-primary',
secondary: 'button-secondary',
},
size: {
s: 'button-s',
m: 'button-m',
l: 'button-l',
},
},
defaultVariants: {
variant: 'primary',
size: 'm',
},
});
interface Props
extends ComponentPropsWithoutRef<'button'>,
VariantProps<typeof buttonVariants> {
children?: ReactNode;
}
export const Button = ({
className,
variant,
size,
children,
...rest
}: Props) => {
return (
<button
className={cn(buttonVariants({ variant, size, className }))}
{...rest}
>
{children}
</button>
);
};
and used like this:
<Button variant="primary" size="l">
Hello
</Button>
or you can use the buttonVariants
helper to create a link that looks like a button:
<a className={buttonVariants({ variant: 'secondary' })}>Click here</a>
shadcn/ui served as an excellent source of inspiration for this approach to styling React apps. It leans more heavily on Tailwind, so if that’s your thing, I recommend checking it out.
The great thing about this approach is that it is flexible, you can use as much or as little of it as you like and without much modification, it can be used in an Astro, Svelte or Vue app. I’ve found that it works well for me, and I hope it works well for you too.
You can see methods from this post used in practice for Otter , my personal bookmarking app side-project.