Design System
Comprehensive design system with Tailwind CSS, shadcn UI components, theming, and accessibility patterns.
By the end of this guide, you'll have mastered the SaaS Boilerplate design system, enabling you to build consistent, accessible, and beautiful user interfaces with Tailwind CSS, shadcn UI components, and proper theming.
Overview
The SaaS Boilerplate includes a comprehensive design system built on modern web standards, featuring Tailwind CSS for styling, shadcn UI for component primitives, Radix UI for accessibility, and a robust theming system. Key features include:
- Tailwind CSS: Utility-first CSS framework with custom design tokens
- shadcn UI: High-quality React components built on Radix UI primitives
- Design Tokens: Consistent colors, typography, spacing, and shadows
- Dark Mode: Complete dark/light theme support with system preference detection
- Accessibility: WCAG-compliant components with proper ARIA attributes
- Responsive Design: Mobile-first approach with breakpoint system
- Animation System: Smooth transitions and micro-interactions
- Type Safety: Full TypeScript support with proper component APIs
- Performance: Optimized bundle size and runtime performance
The system ensures design consistency across your application while providing flexibility for customization and extension.
Architecture
Tailwind CSS Foundation
The design system is built on Tailwind CSS with custom configuration:
// tailwind.config.ts - Custom design tokens
const config: Config = {
theme: {
fontFamily: {
sans: ["geist"],
mono: ["geist-mono"],
},
extend: {
colors: {
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: "hsl(var(--primary))",
// ... extensive color system
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
// ... animations, shadows, spacing
}
}
}shadcn UI Component Library
Components are built using shadcn UI patterns with Radix UI primitives:
// Component structure pattern
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, ...props }, ref) => {
return (
<button
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"CSS Variables and Theming
The system uses CSS custom properties for theming:
/* src/app/globals.css */
:root {
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
/* ... extensive color palette */
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
/* ... dark mode overrides */
}Setting Up the Design System
Install Dependencies
The design system dependencies are pre-installed. Key packages include:
{
"dependencies": {
"tailwindcss": "^3.4.0",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"tailwind-merge": "^2.0.0",
"@radix-ui/react-slot": "^1.0.0",
"next-themes": "^0.2.0"
}
}Configure Tailwind CSS
The Tailwind configuration is already set up with custom design tokens:
// tailwind.config.ts
import type { Config } from "tailwindcss";
const config: Config = {
darkMode: ["class"],
content: [
"./src/**/*.{js,ts,jsx,tsx,mdx}",
"./node_modules/@tremor/**/*.{js,ts,jsx,tsx}",
],
theme: {
// Custom configuration with design tokens
},
plugins: [
require("tailwindcss-animate"),
require("@headlessui/tailwindcss"),
require("@tailwindcss/forms"),
require("@tailwindcss/typography"),
],
};Set Up Theme Provider
The theme provider enables dark mode switching:
// src/app/layout.tsx
import { ThemeProvider } from '@/components/ui/theme-provider'
export default function RootLayout({ children }) {
return (
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
{children}
</ThemeProvider>
)
}Design Tokens
Color System
The design system uses a comprehensive color palette with semantic naming:
Prop
Type
Typography Scale
Typography follows a consistent scale with Geist font family:
Prop
Type
Spacing Scale
Consistent spacing using a rem-based scale:
Prop
Type
Component Patterns
Button Variants
The Button component supports multiple variants and sizes:
// Available variants
type ButtonVariant =
| "default"
| "destructive"
| "outline"
| "secondary"
| "ghost"
| "link"
type ButtonSize = "default" | "sm" | "lg" | "icon"
// Usage
<Button variant="default" size="sm">Click me</Button>
<Button variant="outline" size="lg">Outline</Button>
<Button variant="ghost">Ghost</Button>Form Components
Form components follow consistent patterns with proper validation:
// Form structure
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input placeholder="Enter email" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">Submit</Button>
</form>
</Form>Layout Components
Layout components provide consistent spacing and structure:
// Page layout pattern
<PageWrapper>
<PageHeader>
<PageMainBar>
<Breadcrumb />
</PageMainBar>
<PageActionsBar>
<Button>Create</Button>
</PageActionsBar>
</PageHeader>
<PageBody>
<div className="container max-w-(--breakpoint-md)">
{/* Page content */}
</div>
</PageBody>
</PageWrapper>Theming and Dark Mode
Theme Provider Setup
The theme provider manages light/dark mode switching:
// src/components/ui/theme-provider.tsx
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
}
// Usage in layout
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
{children}
</ThemeProvider>Theme Toggle Component
Theme switching component with system preference detection:
// src/components/ui/theme-toggle.tsx
export function ThemeToggle() {
const { theme, setTheme } = useTheme()
return (
<Button
variant="ghost"
size="icon"
onClick={() => setTheme(theme === "light" ? "dark" : "light")}
>
<Sun className="h-4 w-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-4 w-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
</Button>
)
}CSS Variable Updates
Dynamic theme color updates for mobile browsers:
// Theme color script in theme provider
const THEME_COLOR_SCRIPT = `
(function() {
var html = document.documentElement;
var meta = document.querySelector('meta[name="theme-color"]');
if (!meta) {
meta = document.createElement('meta');
meta.setAttribute('name', 'theme-color');
document.head.appendChild(meta);
}
function updateThemeColor() {
var isDark = html.classList.contains('dark');
meta.setAttribute('content', isDark ? DARK_THEME_COLOR : LIGHT_THEME_COLOR);
}
var observer = new MutationObserver(updateThemeColor);
observer.observe(html, { attributes: true, attributeFilter: ['class'] });
updateThemeColor();
})();
`Practical Examples
Building a Dashboard Card
Complete dashboard card with proper styling and theming:
function DashboardCard({
title,
value,
change,
icon: Icon
}: DashboardCardProps) {
return (
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">{title}</CardTitle>
<Icon className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{value}</div>
<p className="text-xs text-muted-foreground">
{change > 0 ? '+' : ''}{change}% from last month
</p>
</CardContent>
</Card>
)
}
// Usage
<DashboardCard
title="Total Revenue"
value="$45,231.89"
change={20.1}
icon={DollarSign}
/>Form with Validation
Complete form implementation with validation and error handling:
function UserProfileForm() {
const form = useFormWithZod({
schema: userProfileSchema,
defaultValues: {
name: '',
email: '',
bio: '',
},
})
return (
<Form {...form}>
<form onSubmit={form.onSubmit} className="space-y-6">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Full Name</FormLabel>
<FormControl>
<Input placeholder="John Doe" {...field} />
</FormControl>
<FormDescription>
This is your public display name.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input type="email" placeholder="john@example.com" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="bio"
render={({ field }) => (
<FormItem>
<FormLabel>Bio</FormLabel>
<FormControl>
<Textarea
placeholder="Tell us about yourself..."
className="resize-none"
{...field}
/>
</FormControl>
<FormDescription>
You can write up to 500 characters.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" disabled={form.formState.isSubmitting}>
{form.formState.isSubmitting ? 'Saving...' : 'Save Changes'}
</Button>
</form>
</Form>
)
}Data Table with Actions
Advanced data table with sorting, filtering, and actions:
function UsersTable({ users }: { users: User[] }) {
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<div>
<h2 className="text-2xl font-bold tracking-tight">Users</h2>
<p className="text-muted-foreground">
Manage your team members and their permissions.
</p>
</div>
<Button>
<Plus className="mr-2 h-4 w-4" />
Add User
</Button>
</div>
<Card>
<CardHeader>
<CardTitle>Team Members</CardTitle>
<CardDescription>
A list of all users in your organization.
</CardDescription>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Email</TableHead>
<TableHead>Role</TableHead>
<TableHead>Status</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{users.map((user) => (
<TableRow key={user.id}>
<TableCell className="font-medium">{user.name}</TableCell>
<TableCell>{user.email}</TableCell>
<TableCell>
<Badge variant={user.role === 'admin' ? 'default' : 'secondary'}>
{user.role}
</Badge>
</TableCell>
<TableCell>
<Badge variant={user.status === 'active' ? 'default' : 'secondary'}>
{user.status}
</Badge>
</TableCell>
<TableCell className="text-right">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0">
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem>Edit</DropdownMenuItem>
<DropdownMenuItem className="text-destructive">
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
</div>
)
}Accessibility Features
ARIA Attributes
Components include proper ARIA attributes for screen readers:
// Button with accessibility
<Button
aria-label="Close dialog"
aria-expanded={isOpen}
onClick={handleClick}
>
<X className="h-4 w-4" />
</Button>
// Form with proper labeling
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel htmlFor="email">Email Address</FormLabel>
<FormControl>
<Input
id="email"
type="email"
aria-describedby="email-error"
{...field}
/>
</FormControl>
<FormMessage id="email-error" />
</FormItem>
)}
/>Focus Management
Proper focus indicators and keyboard navigation:
// Focus ring styles in globals.css
.focus-visible\:ring-2:focus-visible {
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
}
// Component with focus management
<Dialog>
<DialogTrigger asChild>
<Button>Open Dialog</Button>
</DialogTrigger>
<DialogContent>
{/* Dialog content with proper focus management */}
</DialogContent>
</Dialog>Color Contrast
Design tokens ensure proper contrast ratios:
// Color contrast validation
const colors = {
background: 'oklch(1 0 0)', // Light background
foreground: 'oklch(0.145 0 0)', // Dark text - high contrast
// Dark mode
darkBackground: 'oklch(0.145 0 0)', // Dark background
darkForeground: 'oklch(0.985 0 0)', // Light text - high contrast
}Animation System
CSS Transitions
Smooth transitions for interactive elements:
// Button hover transitions
const buttonVariants = cva(
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
// ... variants
)
// Custom animations in tailwind.config.ts
keyframes: {
"accordion-down": {
from: { height: "0" },
to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: "0" },
},
// ... more animations
}Framer Motion Integration
Advanced animations using Framer Motion:
import { motion } from 'framer-motion'
function AnimatedCard({ children, delay = 0 }) {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{
duration: 0.3,
delay,
ease: 'easeOut'
}}
>
<Card>{children}</Card>
</motion.div>
)
}
// Usage
<AnimatePresence>
{items.map((item, index) => (
<AnimatedCard key={item.id} delay={index * 0.1}>
{item.content}
</AnimatedCard>
))}
</AnimatePresence>Troubleshooting
Best Practices
See Also
- Forms and Validation - Form handling patterns
- SEO - Design system impact on SEO
- Performance and Security - Design system performance considerations
- Routing and Navigation - Navigation patterns
- Styling and UI - Advanced styling techniques
API Reference
Component Variants
Prop
Type
Utility Functions
Prop
Type
CSS Custom Properties
Prop
Type
Animation Classes
Prop
Type