Tailwind Design System
Build scalable, themable Tailwind CSS component libraries using CVA for variants, compound components, design tokens, dark mode, and responsive grids.
Build scalable, themable Tailwind CSS component libraries using CVA for variants, compound components, design tokens, dark mode, and responsive grids.
Real data. Real impact.
Emerging
Developers
Per week
Open source
Skills give you superpowers. Install in 30 seconds.
Build production-ready component libraries with Tailwind CSS using CVA, compound components, design tokens, and theming.
Patterns for scalable Tailwind-based design systems:
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
npx clawhub@latest install tailwind-design-system
// lib/utils.ts import { type ClassValue, clsx } from 'clsx' import { twMerge } from 'tailwind-merge'export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)) }
Primitive Tokens (abstract) └── Semantic Tokens (purpose) └── Component Tokens (specific)Example: slate-900 → foreground → card-title-color
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<HTMLButtonElement>, VariantProps<typeof buttonVariants> { asChild?: boolean }
const Button = forwardRef<HTMLButtonElement, ButtonProps>( ({ className, variant, size, ...props }, ref) => { return ( <button className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} /> ) } ) Button.displayName = 'Button'
export { Button, buttonVariants }
Usage:
<Button variant="destructive" size="lg">Delete</Button> <Button variant="outline">Cancel</Button> <Button variant="ghost" size="icon"><Search /></Button>
Composable components with shared context:
// components/ui/card.tsx import { cn } from '@/lib/utils' import { forwardRef } from 'react'const Card = forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>( ({ className, ...props }, ref) => ( <div ref={ref} className={cn('rounded-lg border bg-card text-card-foreground shadow-sm', className)} {...props} /> ) ) Card.displayName = 'Card'
const CardHeader = forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>( ({ className, ...props }, ref) => ( <div ref={ref} className={cn('flex flex-col space-y-1.5 p-6', className)} {...props} /> ) ) CardHeader.displayName = 'CardHeader'
const CardTitle = forwardRef<HTMLHeadingElement, React.HTMLAttributes<HTMLHeadingElement>>( ({ className, ...props }, ref) => ( <h3 ref={ref} className={cn('text-2xl font-semibold leading-none tracking-tight', className)} {...props} /> ) ) CardTitle.displayName = 'CardTitle'
const CardDescription = forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>( ({ className, ...props }, ref) => ( <p ref={ref} className={cn('text-sm text-muted-foreground', className)} {...props} /> ) ) CardDescription.displayName = 'CardDescription'
const CardContent = forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>( ({ className, ...props }, ref) => ( <div ref={ref} className={cn('p-6 pt-0', className)} {...props} /> ) ) CardContent.displayName = 'CardContent'
const CardFooter = forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>( ({ className, ...props }, ref) => ( <div ref={ref} className={cn('flex items-center p-6 pt-0', className)} {...props} /> ) ) CardFooter.displayName = 'CardFooter'
export { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter }
Usage:
<Card> <CardHeader> <CardTitle>Account Settings</CardTitle> <CardDescription>Manage your account preferences</CardDescription> </CardHeader> <CardContent> <form>{/* form fields */}</form> </CardContent> <CardFooter> <Button>Save Changes</Button> </CardFooter> </Card>
// components/ui/input.tsx import { forwardRef } from 'react' import { cn } from '@/lib/utils'export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> { error?: string }
const Input = forwardRef<HTMLInputElement, InputProps>( ({ className, type, error, ...props }, ref) => { return ( <div className="relative"> <input type={type} className={cn( 'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background', 'file:border-0 file:bg-transparent file:text-sm file:font-medium', 'placeholder:text-muted-foreground', 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2', 'disabled:cursor-not-allowed disabled:opacity-50', error && 'border-destructive focus-visible:ring-destructive', className )} ref={ref} aria-invalid={!!error} aria-describedby={error ?
: undefined} {...props} /> {error && ( <p id={${props.id}-error} className="mt-1 text-sm text-destructive" role="alert"> {error} </p> )} </div> ) } ) Input.displayName = 'Input'${props.id}-errorexport { Input }
// 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<HTMLDivElement>, VariantProps<typeof gridVariants> {}
export function Grid({ className, cols, gap, ...props }: GridProps) { return <div className={cn(gridVariants({ cols, gap, className }))} {...props} /> }
// 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<HTMLDivElement>, VariantProps<typeof containerVariants> {}
export function Container({ className, size, ...props }: ContainerProps) { return <div className={cn(containerVariants({ size, className }))} {...props} /> }
Usage:
<Container> <Grid cols={4} gap="lg"> {products.map(product => ( <ProductCard key={product.id} product={product} /> ))} </Grid> </Container>
// 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<ThemeContextType | undefined>(undefined)
export function ThemeProvider({ children, defaultTheme = 'system', storageKey = 'theme', }: { children: React.ReactNode defaultTheme?: Theme storageKey?: string }) { const [theme, setTheme] = useState<Theme>(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 ( <ThemeContext.Provider value={{ theme, setTheme: (t) => { localStorage.setItem(storageKey, t); setTheme(t) }, resolvedTheme, }}> {children} </ThemeContext.Provider> ) }
export const useTheme = () => { const context = useContext(ThemeContext) if (!context) throw new Error('useTheme must be used within ThemeProvider') return context }
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 ( <Button variant="ghost" size="icon" onClick={() => setTheme(resolvedTheme === 'dark' ? 'light' : 'dark')} > <Sun className="h-5 w-5 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" /> <Moon className="absolute h-5 w-5 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" /> <span className="sr-only">Toggle theme</span> </Button> ) }
// 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')
primary not blue-500)tailwind-merge to handle class conflicts@apply deeply (hurts readability)bg-blue-500 for semantic purposes (use bg-primary)forwardRef on reusable components!important to override styles (fix the cascade instead)No automatic installation available. Please visit the source repository for installation instructions.
View Installation Instructions1,500+ AI skills, agents & workflows. Install in 30 seconds. Part of the Torly.ai family.
© 2026 Torly.ai. All rights reserved.