Comprehensive Theming System for Tailwind CSS

Design System Overview

A comprehensive theming system that supports multiple clients and color modes while maintaining consistency and scalability.

Core Principles

  1. Client-First Architecture: Design system that makes client-specific theming obvious and maintainable.
  2. Dark/Light Mode: Built-in support for color schemes with system preference and manual override.
  3. Design Token Driven: Use CSS custom properties for all theme values.
  4. Type Safety: Leverage TypeScript for theme configuration and validation.
  5. Performance: Critical CSS inlined, non-critical loaded asynchronously.

Design Tokens

Color System

Base Color Scale

full-textdarkestdarkerdarkbaselightlighterlightest
abbreviationxxdkxdkdkbaseltxltxxlt
UsageText-----BGs

Semantic Color Roles

  • Primary: Main brand color, used for primary actions and key elements
  • Secondary: Secondary brand color, used for secondary actions and accents
  • Tertiary: Additional brand color for specific UI elements
  • Accent: Highlight color for important interactive elements
  • Background: Background color for the page
  • Surface: Background color for the content
  • Border: Border color for the content
  • Emphasis: Used to draw attention to important information
  • Warning: Indicates caution or warning states
  • CTA: Call-to-action elements that need to stand out
  • Legible: Ensures text remains readable on any background

Theme Architecture

File Structure

bash
src/

  styles/
    themes/
      base/               # Base design tokens
        colors.css        # Color definitions
        typography.css    # Font families and scales
        spacing.css       # Spacing scale
        breakpoints.css   # Responsive breakpoints
      clients/            # Client-specific overrides
        default/          # Default theme
          light.css       # Light mode variables
          dark.css        # Dark mode variables
        client1/          # Client 1 theme
          light.css
          dark.css
      components/         # Component-specific theming
        buttons.css
        cards.css
        forms.css
    utilities/           # Utility classes
      themes.css         # Theme switching utilities
      typography.css     # Text styles
    global.css           # Global styles and CSS resets

Theme Configuration

Each theme is defined using TypeScript for type safety and better developer experience. The configuration includes both light and dark variants.

Theme Implementation

Theme Configuration (TypeScript)

The theme configuration uses TypeScript interfaces to ensure type safety and autocompletion:
typescript
// src/styles/themes/config.ts

/**
 * Base color interface for theme colors
 */
interface ThemeColors {
  // Brand colors
  primary: string;
  secondary: string;
  tertiary: string;
  accent: string;
  
  // Functional colors
  success: string;
  warning: string;
  danger: string;
  info: string;
  
  // Neutral colors
  background: string;
  surface: string;
  border: string;
  
  // Text colors
  text: {
    primary: string;
    secondary: string;
    disabled: string;
    inverse: string;
  };
}

/**
 * Complete theme interface including both light and dark modes
 */
interface Theme {
  light: ThemeColors;
  dark: ThemeColors;
  typography?: {
    fontFamily: {
      sans: string;
      mono: string;
      display: string;
    };
  };
}

/**
 * Default theme configuration
 */
const defaultTheme: Theme = {
  light: {
    primary: '#684B9E',
    secondary: '#22A6B5',
    tertiary: '#F59C49',
    accent: '#4F46E5',
    success: '#10B981',
    warning: '#F59E0B',
    danger: '#EF4444',
    info: '#3B82F6',
    background: '#FFFFFF',
    surface: '#F9FAFB',
    border: '#E5E7EB',
    text: {
      primary: '#111827',
      secondary: '#4B5563',
      disabled: '#9CA3AF',
      inverse: '#FFFFFF',
    },
  },
  dark: {
    primary: '#8A6AE1',
    secondary: '#4ECDC4',
    tertiary: '#FFA94D',
    accent: '#818CF8',
    success: '#34D399',
    warning: '#FBBF24',
    danger: '#F87171',
    info: '#60A5FA',
    background: '#111827',
    surface: '#1F2937',
    border: '#374151',
    text: {
      primary: '#F9FAFB',
      secondary: '#D1D5DB',
      disabled: '#6B7280',
      inverse: '#111827',
    },
  },
  typography: {
    fontFamily: {
      sans: 'Inter, system-ui, sans-serif',
      mono: 'Fira Code, monospace',
      display: 'Inter, system-ui, sans-serif',
    },
  },
};

/**
 * Client-specific theme overrides
 */
const client1Theme: Theme = {
  ...defaultTheme,
  light: {
    ...defaultTheme.light,
    primary: '#4F46E5',
    secondary: '#10B981',
    accent: '#8B5CF6',
  },
  dark: {
    ...defaultTheme.dark,
    primary: '#818CF8',
    secondary: '#34D399',
    accent: '#A78BFA',
  },
};

/**
 * Export all available themes
 */
export const themes: Record<string, Theme> = {
  default: defaultTheme,
  client1: client1Theme,
  // Add more client themes here
};

/**
 * Get theme configuration for a specific client
 */
export function getTheme(clientId: string = 'default'): Theme {
  return themes[clientId] || defaultTheme;
}

/**
 * Generate CSS variables for a theme
 */
export function generateThemeVars(theme: Theme, mode: 'light' | 'dark' = 'light'): string {
  const colors = theme[mode];
  let cssVars = `:root[data-theme="${mode}"] {\n`;
  
  // Add color variables
  Object.entries(colors).forEach(([key, value]) => {
    if (typeof value === 'string') {
      cssVars += `  --color-${key}: ${value};\n`;
    } else if (typeof value === 'object' && value !== null) {
      // Handle nested objects (like text colors)
      Object.entries(value).forEach(([nestedKey, nestedValue]) => {
        cssVars += `  --color-${key}-${nestedKey}: ${nestedValue};\n`;
      });
    }
  });
  
  cssVars += '}';
  return cssVars;
}

Implementation Status (August 2025)

Actual Implementation Details

What Was Built

The theme system was successfully implemented in /home/mps/code/lossless-monorepo/astro-knots/twf-site/ with the following architecture:

File Structure (As Implemented)

bash
src/
  styles/
    global.css           # Main CSS with Tailwind imports and dark mode overrides
    water-theme.css      # CSS custom properties for both themes
  utils/
    theme-switcher.js    # Theme toggle utility (default ↔ water)
    mode-switcher.js     # Mode toggle utility (light ↔ dark)
  pages/
    index.astro          # Demo page with toggle buttons

Theme System Architecture

Two-Layer System:
  1. Theme Layer: default vs water (controlled by data-theme="water" attribute)
  2. Mode Layer: light vs dark (controlled by data-mode="dark" attribute)

CSS Custom Properties Implementation

water-theme.css defines CSS custom properties for both themes:
  • :root - Default theme colors (Tailwind defaults)
  • [data-theme="water"] - Water theme colors (inverted/ocean blues)
global.css handles:
  • Tailwind CSS imports
  • Dark mode overrides using [data-mode="dark"] selectors
  • CSS specificity fixes with !important declarations

JavaScript Utilities

ThemeSwitcher Class:
  • Toggles between default and water themes
  • Uses data-theme attribute on <html>
  • Persists preference in localStorage
  • Provides methods: toggleTheme(), setTheme(), getCurrentTheme()
ModeSwitcher Class:
  • Toggles between light and dark modes
  • Uses data-mode attribute on <html>
  • Persists preference in localStorage
  • Provides methods: toggleMode(), setMode(), getCurrentMode()

Integration with Astro

CSS Import:
javascript
// In .astro frontmatter
import '../styles/global.css';
JavaScript Integration:
javascript
import { themeSwitcher } from '../utils/theme-switcher.js';
import { modeSwitcher } from '../utils/mode-switcher.js';

Key Implementation Challenges & Solutions

1. CSS Specificity Issues

Problem: Tailwind utility classes weren't being overridden by dark mode styles. Solution: Used !important declarations and specific selectors like [data-mode="dark"] .bg-primary-500.

2. CSS Loading Order

Problem: CSS wasn't loading in Astro pages. Solution: Explicit CSS import in Astro frontmatter: import '../styles/global.css';

3. Button Visibility in Dark Mode

Problem: Dark mode CSS was making button text invisible. Solution: Proper contrast handling with [data-mode="dark"] .text-white overrides.

Testing Implementation

Comprehensive Test Suite:
  • 33 passing tests covering all functionality
  • Unit tests for both ThemeSwitcher and ModeSwitcher classes
  • Integration tests with JSDOM for DOM interactions
  • Vitest configuration with proper setup files
Test Files:
  • src/utils/__tests__/theme-switcher.test.js
  • src/utils/__tests__/mode-switcher.test.js
  • src/utils/__tests__/toggle-integration.test.js
  • vitest.config.js

Working Combinations

The system provides 4 distinct visual states:
  1. Default + Light - Standard Tailwind colors, light backgrounds
  2. Default + Dark - Standard colors with dark backgrounds/light text
  3. Water + Light - Ocean blue theme, light backgrounds
  4. Water + Dark - Ocean blue theme with dark backgrounds/light text

Usage Example

html
<!-- Toggle buttons -->
<button id="theme-toggle">Toggle to Water Theme</button>
<button id="mode-toggle">Toggle to Dark Mode</button>

<script>
import { themeSwitcher } from '../utils/theme-switcher.js';
import { modeSwitcher } from '../utils/mode-switcher.js';

// Theme toggle
document.getElementById('theme-toggle').addEventListener('click', () => {
  themeSwitcher.toggleTheme();
});

// Mode toggle  
document.getElementById('mode-toggle').addEventListener('click', () => {
  modeSwitcher.toggleMode();
});
</script>

Lessons Learned

  1. CSS Import Order Matters: In Astro, CSS must be explicitly imported in frontmatter
  2. Specificity is Critical: Dark mode overrides need !important to override Tailwind utilities
  3. Two-Layer Architecture Works: Separating theme (colors) from mode (light/dark) provides flexibility
  4. localStorage Integration: Persisting preferences enhances user experience
  5. Comprehensive Testing: Both unit and integration tests are essential for theme systems

Future Enhancements

  • System Preference Detection: Auto-detect user's OS dark/light preference
  • Smooth Transitions: Add CSS transitions between theme/mode changes
  • More Themes: Extend beyond default/water to support multiple clients
  • Component-Level Theming: Theme-aware component variants

Tailwind CSS v4 Migration (August 2025)

Critical Updates Made

The theme system was successfully migrated from Tailwind CSS v3 to v4 with the following key changes:

1. Configuration Migration

  • Removed: tailwind.config.js (v3 JavaScript configuration)
  • Added: CSS-based configuration using @theme directive in global.css
  • Updated: Astro config to use @tailwindcss/vite plugin instead of @astrojs/tailwind

2. CSS Architecture Changes

Before (v3 style):
css
@layer theme {
  :root {
    --color-primary-50: 250 250 250; /* RGB space-separated */
  }
}
After (v4 style):
css
@theme {
  --color-primary-50: #fafafa; /* Hex format */
  --color-primary-100: #f4f4f5;
  /* ... complete color scale */
}

3. Theme Override Implementation

Water Theme Overrides:
css
.theme-water {
  --color-primary-50: #ecfeff;
  --color-primary-100: #cffafe;
  --color-primary-200: #a5f3fc;
  --color-primary-300: #67e8f9;
  --color-primary-400: #22d3ee;
  --color-primary-500: #06b6d4;
  --color-primary-600: #0891b2;
  --color-primary-700: #0e7490;
  --color-primary-800: #155e75;
  --color-primary-900: #164e63;
  --color-primary-950: #083344;
  /* ... secondary and accent colors */
}

4. TypeScript Support Added

Created src/types/tailwind.d.ts for better IDE support:
typescript
declare global {
  namespace CSS {
    interface AtRules {
      theme: string;
    }
  }
}

export interface ThemeColors {
  primary: { 50: string; 100: string; /* ... */ };
  secondary: { 50: string; 100: string; /* ... */ };
  accent: { 50: string; 100: string; /* ... */ };
}

5. Astro Configuration Update

Updated astro.config.mjs:
javascript
import { defineConfig } from 'astro/config';
import tailwindcss from '@tailwindcss/vite';

export default defineConfig({
  vite: {
    plugins: [tailwindcss()],
  },
});

Current Status & Known Issues

✅ Working Features

  • Theme switching between default and water themes
  • Color variables properly defined in Tailwind v4 format
  • TypeScript support for better development experience
  • Astro integration with Vite plugin

⚠️ Known Issues (RESOLVED)

  • json
    {
      "type": "delete",
      "children": [
        {
          "type": "strong",
          "children": [
            {
              "type": "text",
              "value": "Default theme colors not displaying",
              "position": {
                "start": {
                  "line": 469,
                  "column": 7,
                  "offset": 13140
                },
                "end": {
                  "line": 469,
                  "column": 42,
                  "offset": 13175
                }
              }
            }
          ],
          "position": {
            "start": {
              "line": 469,
              "column": 5,
              "offset": 13138
            },
            "end": {
              "line": 469,
              "column": 44,
              "offset": 13177
            }
          }
        },
        {
          "type": "text",
          "value": ": Colors work in water theme but show as white squares in default theme",
          "position": {
            "start": {
              "line": 469,
              "column": 44,
              "offset": 13177
            },
            "end": {
              "line": 469,
              "column": 115,
              "offset": 13248
            }
          }
        }
      ],
      "position": {
        "start": {
          "line": 469,
          "column": 3,
          "offset": 13136
        },
        "end": {
          "line": 469,
          "column": 117,
          "offset": 13250
        }
      }
    }
    FIXED
  • CSS lint warnings: IDE shows "Unknown at rule @theme" (expected, as CSS linters don't recognize Tailwind v4 directives)

🔧 Issue Resolution (August 12, 2025)

Problem Identified: The default theme colors were not displaying properly due to a CSS variable format mismatch:
  • Default theme was using space-separated RGB values: --color-primary-50: 250 250 250;
  • Water theme was using hex values: --color-primary-50: #ecfeff;
  • This inconsistency caused the default theme colors to not render properly
Solution Applied: Updated /Users/mpstaton/code/lossless-monorepo/astro-knots/twf-site/src/styles/global.css to use consistent hex format for all color variables:
css
/* BEFORE - Space-separated RGB (not working) */
.theme-default {
  --color-primary-50: 250 250 250;
  --color-primary-100: 244 244 245;
  /* ... */
}

/* AFTER - Hex format (working) */
.theme-default {
  --color-primary-50: #fafafa;
  --color-primary-100: #f4f4f5;
  /* ... */
}
Root Cause: The issue occurred during the Tailwind CSS v4 migration where different color formats were mixed. Tailwind v4 expects consistent color value formats across all theme definitions.
Verification:
  • Theme toggle now works correctly between default and water themes
  • All color variables display properly in both themes
  • Console logs confirm theme switching functionality is working

Migration Lessons Learned

  1. CSS-First Configuration: Tailwind v4's move to CSS-based config requires different mental model
  2. Hex vs RGB Format: v4 prefers hex colors over space-separated RGB values
  3. Plugin Changes: Vite plugin integration differs significantly from v3
  4. IDE Support: Additional TypeScript definitions needed for proper linting
  5. Theme Inheritance: CSS custom property overrides work well for theme switching