ADR-005: Tailwind CSS with Shadcn/ui
Status
Accepted
Context
The application needs a styling approach that:
- Supports theming: Multi-tenant branding
- Provides components: Pre-built, accessible UI primitives
- Minimizes bundle size: No unused CSS
- Developer experience: Fast iteration, good DX
Options considered:
- CSS Modules: Scoped styles but no design system
- Styled Components: Runtime overhead, CSS-in-JS complexity
- Tailwind + Headless UI: Utility-first with accessibility
- Tailwind + Shadcn/ui: Copy-paste components, full control
Decision
We will use Tailwind CSS with Shadcn/ui components:
- Tailwind CSS: Utility-first styling, purged for production
- Shadcn/ui: Copy-paste components (not npm dependency)
- CSS Variables: Theming via custom properties
- cn() utility: Conditional class merging
// src/lib/utils.ts
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
Component Pattern
// src/components/ui/button.tsx
import { cn } from '@/lib/utils';
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'default' | 'destructive' | 'outline';
}
export function Button({ className, variant = 'default', ...props }: ButtonProps) {
return (
<button
className={cn(
'inline-flex items-center justify-center rounded-md',
variant === 'default' && 'bg-primary text-primary-foreground',
variant === 'destructive' && 'bg-destructive text-destructive-foreground',
className
)}
{...props}
/>
);
}
Theming
CSS variables enable runtime theming:
/* globals.css */
:root {
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
}
[data-theme="dark"] {
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
}
Consequences
Positive
- Full control: Components are in your codebase
- No runtime: Styles compiled at build time
- Type-safe: TypeScript props and variants
- Accessible: Radix UI primitives
Negative
- Verbose HTML: Many utility classes
- Learning curve: Tailwind class names
- Manual updates: Shadcn components don't auto-update
Neutral
- Bundle size: Purged CSS is minimal
- Design consistency: Must maintain your own design tokens
Related ADRs
- ADR-001: Config-Driven - Component theming via config