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
- Client-First Architecture: Design system that makes client-specific theming obvious and maintainable.
- Dark/Light Mode: Built-in support for color schemes with system preference and manual override.
- Design Token Driven: Use CSS custom properties for all theme values.
- Type Safety: Leverage TypeScript for theme configuration and validation.
- Performance: Critical CSS inlined, non-critical loaded asynchronously.
Design Tokens
Color System
Base Color Scale
| full-text | darkest | darker | dark | base | light | lighter | lightest |
| abbreviation | xxdk | xdk | dk | base | lt | xlt | xxlt |
| Usage | Text | - | - | - | - | - | 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:
- Theme Layer:
defaultvswater(controlled bydata-theme="water"attribute) - Mode Layer:
lightvsdark(controlled bydata-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
!importantdeclarations
JavaScript Utilities
ThemeSwitcher Class:
- Toggles between
defaultandwaterthemes - Uses
data-themeattribute on<html> - Persists preference in localStorage
- Provides methods:
toggleTheme(),setTheme(),getCurrentTheme()
ModeSwitcher Class:
- Toggles between
lightanddarkmodes - Uses
data-modeattribute 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.jssrc/utils/__tests__/mode-switcher.test.jssrc/utils/__tests__/toggle-integration.test.jsvitest.config.js
Working Combinations
The system provides 4 distinct visual states:
- Default + Light - Standard Tailwind colors, light backgrounds
- Default + Dark - Standard colors with dark backgrounds/light text
- Water + Light - Ocean blue theme, light backgrounds
- 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
- CSS Import Order Matters: In Astro, CSS must be explicitly imported in frontmatter
- Specificity is Critical: Dark mode overrides need
!importantto override Tailwind utilities - Two-Layer Architecture Works: Separating theme (colors) from mode (light/dark) provides flexibility
- localStorage Integration: Persisting preferences enhances user experience
- 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
@themedirective inglobal.css - Updated: Astro config to use
@tailwindcss/viteplugin 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)
- FIXEDjson
{ "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 } } } - 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
- CSS-First Configuration: Tailwind v4's move to CSS-based config requires different mental model
- Hex vs RGB Format: v4 prefers hex colors over space-separated RGB values
- Plugin Changes: Vite plugin integration differs significantly from v3
- IDE Support: Additional TypeScript definitions needed for proper linting
- Theme Inheritance: CSS custom property overrides work well for theme switching