🎁 Get the FREE AI Skills Starter Guide β€” Subscribe β†’
BytesAgainBytesAgain
πŸ¦€ ClawHub

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.

Versionv1.0.0
Downloads5,945
Installs67
Stars⭐ 8
TERMINAL
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:

  • 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, VariantProps { asChild?: boolean }

    const Button = forwardRef( ({ className, variant, size, ...props }, ref) => { return (

    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
      
      
        
    {/* form fields */}


    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

  • 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)
  • πŸ“‹ Tips & 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