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.

BySourav Mishra3 min read

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:

  1. use cn to fix conflicts.
  2. use cva for component states.
  3. 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.

Share this post

Cover image for how to scale tailwind css without losing your mind

You might also like

See all