install
source · Clone the upstream repo
git clone https://github.com/wpank/ai
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/wpank/ai "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/frontend/tailwind-design-system" ~/.claude/skills/wpank-ai-tailwind-design-system && rm -rf "$T"
manifest:
skills/frontend/tailwind-design-system/SKILL.mdsource content
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:
- Class Variance Authority (CVA) for type-safe variants
- Compound component architecture
- CSS variable-based theming
- Dark mode implementation
- Responsive grid systems
- Animation utilities
WHEN
- Building a component library with Tailwind
- Implementing design tokens and theming
- Creating reusable UI components with variants
- Setting up dark mode
- Standardizing patterns across a codebase
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<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>
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<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>
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<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 ? `${props.id}-error` : undefined} {...props} /> {error && ( <p id={`${props.id}-error`} className="mt-1 text-sm text-destructive" role="alert"> {error} </p> )} </div> ) } ) 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<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>
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<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 }
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 ( <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> ) }
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
- Use CSS variables for theming (enables runtime switching)
- Compose variants with CVA (type-safe, explicit)
- Use semantic color names (
notprimary
)blue-500 - Forward refs for composition
- Add accessibility attributes (ARIA, focus states)
- Use
to handle class conflictstailwind-merge
Don't
- Use arbitrary values when you can extend the theme
- Nest
deeply (hurts readability)@apply - Skip focus states (keyboard users need them)
- Hardcode colors (use semantic tokens)
- Forget to test dark mode
NEVER
- Use hardcoded colors like
for semantic purposes (usebg-blue-500
)bg-primary - Skip focus-visible styles on interactive elements
- Mix arbitrary values with design tokens inconsistently
- Forget
on reusable componentsforwardRef - Use
to override styles (fix the cascade instead)!important