how to scale tailwind css without losing your mind - By Sourav Mishra (@souravvmishra)
tired of messy tailwind code? let's talk about the cn utility, cva, and keeping things clean.
okeyy, so let's see how i keep my tailwind css clean. i am sourav mishra, and i hate messy code.
tailwind is super fast to write. but without a plan, your components look like a giant bowl of alphabet soup. lol. the fix isn't dropping tailwind—it's just organizing it better.
the magic cn utility
first things first, we need a safe way to merge class names. you can't just stick strings together because tailwind classes will fight each other (like p-4 vs p-8).
why do they fight? well, css is tricky like that.
run this: npm i clsx tailwind-merge
- clsx: a tiny tool to add classes conditionally.
- tailwind-merge: fixes the class conflicts.
make a file for it:
// lib/utils.ts
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
now you can override stuff safely:
// this outputs "bg-red-500", kicking out the blue one
<div className={cn("bg-blue-500", "bg-red-500")} />
keeping variants tidy with cva
class variance authority (cva) is a lifesaver. it lets you build "recipes" for your components.
install it: npm i class-variance-authority
// components/Button.tsx
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {}
export function Button({ className, variant, size, ...props }: ButtonProps) {
return (
<button
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
}
this is exactly how shadcn/ui does it. it keeps all the ugly class strings in one neat place.
using variables for colors
please don't hardcode hex codes like #3b82f6 everywhere. use variables in your globals.css and hook them up in tailwind.config.ts.
globals.css:
@layer base {
:root {
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
}
.dark {
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
}
}
tailwind.config.ts:
theme: {
extend: {
colors: {
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
},
},
}
now just use bg-primary. it handles dark mode automatically. no more typing dark:bg-white a million times!
wrapping up
keeping tailwind clean comes down to this:
- use
cnto fix conflicts. - use cva for component states.
- use css variables for themes.
wanna see how to validate forms with these? read my zod guide.
quick questions
q: why not just use regular style props? inline styles are hard to reuse. and they suck at hover or focus states.
q: is cva better than styled-components? if you use tailwind, yep. you get the same nice props without the heavy javascript runtime.
written by sourav mishra, trying to keep codebases clean.