Skill: wpank/tailwind-design-system (version 1.0.0)

--- 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: - 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 ```bash npx clawhub@latest install tailwind-design-system ``` --- ## Core Setup ### Utility Function ```typescript // 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: ```typescript // 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:** ```tsx <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: ```typescript // 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:** ```tsx <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 ```typescript // 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 ```typescript // 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:** ```tsx <Container> <Grid cols={4} gap="lg"> {products.map(product => ( <ProductCard key={product.id} product={product} /> ))} </Grid> </Container> ``` --- ## Pattern 5: Dark Mode ### Theme Provider ```typescript // 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 ```tsx 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 ```typescript // 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 (`primary` not `blue-500`) - Forward refs for composition - Add accessibility attributes (ARIA, focus states) - Use `tailwind-merge` to handle class conflicts ### Don't - Use arbitrary values when you can extend the theme - Nest `@apply` deeply (hurts readability) - Skip focus states (keyboard users need them) - Hardcode colors (use semantic tokens) - Forget to test dark mode --- ## NEVER - Use hardcoded colors like `bg-blue-500` for semantic purposes (use `bg-primary`) - Skip focus-visible styles on interactive elements - Mix arbitrary values with design tokens inconsistently - Forget `forwardRef` on reusable components - Use `!important` to override styles (fix the cascade instead)


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: clean (LOW)