Tailwind Design System
by @wpank
Build scalable, themable Tailwind CSS component libraries using CVA for variants, compound components, design tokens, dark mode, and responsive grids.
clawhub install tailwind-design-systemπ About This Skill
name: tailwind-design-system model: fast
Tailwind Design System
Build production-ready component libraries with Tailwind CSS using CVA, compound components, design tokens, and theming.
WHAT
Patterns for scalable Tailwind-based design systems:
WHEN
KEYWORDS
tailwind, cva, design system, component library, variants, theming, dark mode, design tokens, shadcn, compound components, tailwind-merge
Related skills: tailwind-v4-shadcn for Tailwind v4 setup and migration
Installation
OpenClaw / Moltbot / Clawbot
npx clawhub@latest install tailwind-design-system
Core Setup
Utility Function
// lib/utils.ts
import { type ClassValue, clsx } from 'clsx'
import { twMerge } from 'tailwind-merge'export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
Design Token Architecture
Primitive Tokens (abstract)
βββ Semantic Tokens (purpose)
βββ Component Tokens (specific)Example:
slate-900 β foreground β card-title-color
Pattern 1: CVA Components
Class Variance Authority for type-safe, variant-based components:
// components/ui/button.tsx
import { cva, type VariantProps } from 'class-variance-authority'
import { forwardRef } from 'react'
import { cn } from '@/lib/utils'const buttonVariants = cva(
// Base styles (always applied)
'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50',
{
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',
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground',
link: 'text-primary underline-offset-4 hover:underline',
},
size: {
default: 'h-10 px-4 py-2',
sm: 'h-9 rounded-md px-3',
lg: 'h-11 rounded-md px-8',
icon: 'h-10 w-10',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes,
VariantProps {
asChild?: boolean
}
const Button = forwardRef(
({ className, variant, size, ...props }, ref) => {
return (
)
}
)
Button.displayName = 'Button'
export { Button, buttonVariants }
Usage:
Pattern 2: Compound Components
Composable components with shared context:
// components/ui/card.tsx
import { cn } from '@/lib/utils'
import { forwardRef } from 'react'const Card = forwardRef>(
({ className, ...props }, ref) => (
)
)
Card.displayName = 'Card'
const CardHeader = forwardRef>(
({ className, ...props }, ref) => (
)
)
CardHeader.displayName = 'CardHeader'
const CardTitle = forwardRef>(
({ className, ...props }, ref) => (
)
)
CardTitle.displayName = 'CardTitle'
const CardDescription = forwardRef>(
({ className, ...props }, ref) => (
)
)
CardDescription.displayName = 'CardDescription'
const CardContent = forwardRef>(
({ className, ...props }, ref) => (
)
)
CardContent.displayName = 'CardContent'
const CardFooter = forwardRef>(
({ className, ...props }, ref) => (
)
)
CardFooter.displayName = 'CardFooter'
export { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter }
Usage:
Account Settings
Manage your account preferences
Pattern 3: Form Components with Validation
// components/ui/input.tsx
import { forwardRef } from 'react'
import { cn } from '@/lib/utils'export interface InputProps extends React.InputHTMLAttributes {
error?: string
}
const Input = forwardRef(
({ className, type, error, ...props }, ref) => {
return (
${props.id}-error : undefined}
{...props}
/>
{error && (
${props.id}-error} className="mt-1 text-sm text-destructive" role="alert">
{error}
)}
)
}
)
Input.displayName = 'Input'
export { Input }
Pattern 4: Grid System
// components/ui/grid.tsx
import { cn } from '@/lib/utils'
import { cva, type VariantProps } from 'class-variance-authority'const gridVariants = cva('grid', {
variants: {
cols: {
1: 'grid-cols-1',
2: 'grid-cols-1 sm:grid-cols-2',
3: 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3',
4: 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-4',
},
gap: {
none: 'gap-0',
sm: 'gap-2',
md: 'gap-4',
lg: 'gap-6',
xl: 'gap-8',
},
},
defaultVariants: {
cols: 3,
gap: 'md',
},
})
interface GridProps extends React.HTMLAttributes, VariantProps {}
export function Grid({ className, cols, gap, ...props }: GridProps) {
return
}// Container component
const containerVariants = cva('mx-auto w-full px-4 sm:px-6 lg:px-8', {
variants: {
size: {
sm: 'max-w-screen-sm',
md: 'max-w-screen-md',
lg: 'max-w-screen-lg',
xl: 'max-w-screen-xl',
'2xl': 'max-w-screen-2xl',
full: 'max-w-full',
},
},
defaultVariants: {
size: 'xl',
},
})
interface ContainerProps extends React.HTMLAttributes, VariantProps {}
export function Container({ className, size, ...props }: ContainerProps) {
return
}
Usage:
{products.map(product => (
))}
Pattern 5: Dark Mode
Theme Provider
// providers/theme-provider.tsx
'use client'import { createContext, useContext, useEffect, useState } from 'react'
type Theme = 'dark' | 'light' | 'system'
interface ThemeContextType {
theme: Theme
setTheme: (theme: Theme) => void
resolvedTheme: 'dark' | 'light'
}
const ThemeContext = createContext(undefined)
export function ThemeProvider({
children,
defaultTheme = 'system',
storageKey = 'theme',
}: {
children: React.ReactNode
defaultTheme?: Theme
storageKey?: string
}) {
const [theme, setTheme] = useState(defaultTheme)
const [resolvedTheme, setResolvedTheme] = useState<'dark' | 'light'>('light')
useEffect(() => {
const stored = localStorage.getItem(storageKey) as Theme | null
if (stored) setTheme(stored)
}, [storageKey])
useEffect(() => {
const root = window.document.documentElement
root.classList.remove('light', 'dark')
const resolved = theme === 'system'
? window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
: theme
root.classList.add(resolved)
setResolvedTheme(resolved)
}, [theme])
return (
{ localStorage.setItem(storageKey, t); setTheme(t) },
resolvedTheme,
}}>
{children}
)
}
export const useTheme = () => {
const context = useContext(ThemeContext)
if (!context) throw new Error('useTheme must be used within ThemeProvider')
return context
}
Theme Toggle
import { Moon, Sun } from 'lucide-react'
import { useTheme } from '@/providers/theme-provider'
import { Button } from '@/components/ui/button'export function ThemeToggle() {
const { resolvedTheme, setTheme } = useTheme()
return (
)
}
Animation Utilities
// lib/animations.ts
import { cn } from './utils'export const fadeIn = 'animate-in fade-in duration-300'
export const fadeOut = 'animate-out fade-out duration-300'
export const slideInFromTop = 'animate-in slide-in-from-top duration-300'
export const slideInFromBottom = 'animate-in slide-in-from-bottom duration-300'
export const zoomIn = 'animate-in zoom-in-95 duration-300'
export const zoomOut = 'animate-out zoom-out-95 duration-300'
// Compound animations
export const modalEnter = cn(fadeIn, zoomIn, 'duration-200')
export const modalExit = cn(fadeOut, zoomOut, 'duration-200')
export const dropdownEnter = cn(fadeIn, slideInFromTop, 'duration-150')
Best Practices
Do
primary not blue-500)tailwind-merge to handle class conflictsDon't
@apply deeply (hurts readability)NEVER
bg-blue-500 for semantic purposes (use bg-primary)forwardRef on reusable components!important to override styles (fix the cascade instead)π Tips & Best Practices
Do
primary not blue-500)tailwind-merge to handle class conflictsDon't
@apply deeply (hurts readability)