Tailwind CSS v4: Building a SaaS Design System in One Day
CSS tokens, dark mode, reusable components: how to structure a professional design system with Tailwind v4 and shadcn/ui.
Why a design system from day one?
A SaaS without a design system always ends up the same way: hardcoded colors everywhere, a broken dark mode, and components copy-pasted instead of reused.
With Tailwind CSS v4 and shadcn/ui, you can put a solid system in place in under a day. Here's how HeartCo did it.
Step 1: CSS tokens
Tailwind v4 introduces native CSS tokens. No more tailwind.config.js for colors: everything goes through CSS variables:
/* globals.css */
@theme {
--color-brand: #4f46e5;
--color-deep-navy: #0d1b2a;
--color-ai-blue: #3b82f6;
--color-ai-cyan: #06b6d4;
}The payoff: dark mode becomes trivial
:root {
--color-background: oklch(0.984 0.003 247.858);
--color-foreground: oklch(0.141 0.005 285.823);
}
.dark {
--color-background: oklch(0.141 0.005 285.823);
--color-foreground: oklch(0.984 0.003 247.858);
}Your components use bg-background and text-foreground, and the theme switches automatically. Zero dark: prefixes to manage by hand.
Step 2: the component library
shadcn/ui isn't an npm dependency, it's a code generator. You copy the components into your project and customize them:
npx shadcn@latest add button card dialogThe secret: never modify the primitives directly. Build business-specific wrappers instead:
// src/components/ui/heart-button.tsx
import { Button, type ButtonProps } from "~/components/ui/button";
import { cn } from "~/lib/utils";
export function HeartButton({
variant = "default",
className,
...props
}: ButtonProps) {
return (
<Button
className={cn(
"transition-all duration-200",
"hover:scale-[1.02] active:scale-[0.98]",
variant === "primary" &&
"bg-brand text-white shadow-[0_0_20px_rgba(99,102,241,0.3)]",
className,
)}
{...props}
/>
);
}Step 3: the cn() function, your best friend
// src/lib/utils.ts
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}cn() solves Tailwind's classic problem: when two conflicting classes meet (p-4 + p-6), twMerge keeps the last one. Without it, style overrides are unpredictable.
Step 4: the design system's rules
A few rules HeartCo applies strictly:
- Never hardcode colors: always use tokens (
text-foreground, nottext-gray-900) - Never use a
dark:prefix, the tokens handle the theme - Hover states are mandatory: every interactive element gives visual feedback
- Cards use shadows, not borders: more modern, more elegant
- Headings use a display font: visually separate heading from body
The result
With this structure, adding a new component takes 5 minutes instead of 30. Dark mode works everywhere. And your SaaS looks professional from day one.
Go further
Ready to launch your SaaS?
HeartCo Starter includes everything you need: auth, payments, AI, mobile, audited security. Starting at $219.