Tailwind CSS v4 moved to a CSS-first configuration and a faster engine. This guide covers the patterns and tools that keep a Tailwind codebase clean, scalable, and good-looking.
Tailwind CSS v4
Version 4 is configured entirely in CSS. There is no tailwind.config.js; you just import it and declare your design tokens.
@import "tailwindcss";
@theme {
--color-primary: #6366f1;
--font-sans: 'Inter', ui-sans-serif, system-ui, sans-serif;
--radius-lg: 0.75rem;
}Custom utilities and variants are declared in the same file.
@utility container {
margin-inline: auto;
padding-inline: 1rem;
}
@custom-variant dark (&:where(.dark, .dark *));Design tokens
Define tokens once and reference them everywhere, so colors and spacing stay consistent. Arbitrary values like bg-[#6366f1] are for one-offs, not a design system.
@theme {
--color-primary-500: #6366f1;
--color-primary-600: #4f46e5;
--spacing-18: 4.5rem;
}Then bg-primary-500, p-18, and text-primary-600 all work.
Utility-first best practices
Compose small utilities instead of writing bespoke CSS, but extract a pattern the moment it repeats.
<button class="inline-flex items-center gap-2 rounded-lg bg-primary-600 px-4 py-2 text-sm font-medium text-white hover:bg-primary-500">
Save
</button>Rules that keep it maintainable:
- Reuse tokens, not raw values.
- Sort classes automatically (see the Prettier plugin below).
- Prefer
gap/space-*over manual margins. - Keep responsive and state variants close to the base classes.
Reusable components
When a pattern repeats, extract it. Tailwind offers three levels.
Component classes with @apply:
.btn {
@apply inline-flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium;
}
.btn-primary {
@apply bg-primary-600 text-white hover:bg-primary-500;
}Class variants with class-variance-authority (great for React/Solid):
import { cva, type VariantProps } from 'class-variance-authority'
const button = cva('inline-flex items-center rounded-lg font-medium', {
variants: {
variant: {
primary: 'bg-primary-600 text-white hover:bg-primary-500',
ghost: 'border border-gray-300 text-gray-700',
},
size: {
sm: 'px-3 py-1.5 text-sm',
md: 'px-4 py-2 text-base',
},
},
defaultVariants: { variant: 'primary', size: 'md' },
})Merging classes with clsx + tailwind-merge:
import { clsx, type ClassValue } from 'clsx'
import { twMerge } from 'tailwind-merge'
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
// later conflicting class wins
cn('px-4', 'px-2') // 'px-2'The component ecosystem
You do not have to build every primitive from scratch.
- shadcn/ui — copy-paste, Radix-based components you own and edit.
- Radix UI / Headless UI — accessible, unstyled primitives.
- daisyUI — ready-made, themeable component classes.
Pair them with Tailwind for full control without the accessibility burden.
Tooling
@tailwindcss/vite— the Vite plugin (add it once, forget it).prettier-plugin-tailwindcss— sorts utility classes for a consistent order.clsx+tailwind-merge— compose and dedupe classes.class-variance-authority— typed component variants.
npm install -D tailwindcss @tailwindcss/vite prettier-plugin-tailwindcss
npm install clsx tailwind-merge class-variance-authorityMaking it beautiful
Beautiful UI is mostly consistency, not decoration.
- Typography — one font scale, comfortable line height, constrained measure.
- Spacing — stick to the 4px scale; align to a grid.
- Color — a small palette with one accent; check contrast for text.
- Motion — short, subtle transitions (
transition,duration-150). - Dark mode — design both, driven by the
darkvariant.
Ready production examples
A complete, copy-pasteable setup: design tokens, a cn helper, and typed components.
Theme tokens (theme.css):
@import "tailwindcss";
@theme {
--color-brand-50: #eef2ff;
--color-brand-500: #6366f1;
--color-brand-600: #4f46e5;
--color-brand-700: #4338ca;
--color-surface: #ffffff;
--color-ink: #0f172a;
--color-muted: #64748b;
--font-sans: 'Inter', ui-sans-serif, system-ui, sans-serif;
--shadow-card: 0 1px 2px rgb(15 23 42 / 0.06), 0 4px 12px rgb(15 23 42 / 0.08);
}
@custom-variant dark (&:where(.dark, .dark *));Button (button.tsx) with typed variants:
import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from './cn'
const buttonVariants = cva(
'inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500 disabled:pointer-events-none disabled:opacity-50',
{
variants: {
variant: {
primary: 'bg-brand-600 text-white hover:bg-brand-500',
secondary: 'border border-slate-200 bg-surface text-ink hover:bg-slate-50',
ghost: 'text-ink hover:bg-slate-100',
destructive: 'bg-red-600 text-white hover:bg-red-500',
},
size: {
sm: 'h-8 px-3 text-xs',
md: 'h-10 px-4',
lg: 'h-12 px-6 text-base',
},
},
defaultVariants: { variant: 'primary', size: 'md' },
},
)
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}
/>
)
}Card and Input (card.tsx, input.tsx):
export function Card({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn(
'rounded-md border border-slate-200 bg-surface p-6 shadow-card',
className,
)}
{...props}
/>
)
}export function Input({ className, ...props }: React.InputHTMLAttributes<HTMLInputElement>) {
return (
<input
className={cn(
'h-10 w-full rounded-md border border-slate-200 bg-surface px-3 text-sm text-ink placeholder:text-muted focus:outline-none focus:ring-2 focus:ring-brand-500',
className,
)}
{...props}
/>
)
}These tokens and components form the base of a real design system: consistent colors, typed variants, and a single source of truth.
Wrapping up
Tailwind v4's CSS-first setup, design tokens, and component extraction (with cva and cn) give you a system that scales. Add shadcn/Radix for primitives and the Prettier plugin for order, and you get beautiful, maintainable UI.