Theming System
The Checkstack UI system uses a centralized, configurable theming architecture based on CSS Custom Properties (CSS variables) and HSL color space. This system provides automatic dark mode support, semantic color tokens, and consistent styling across the entire platform.
Architecture Overview
Section titled “Architecture Overview”The theming system consists of four key layers:
- CSS Variables (
core/ui/src/themes.css) - Core color definitions using HSL values - Tailwind Integration (
tailwind.config.js) - Maps CSS variables to Tailwind utility classes - Theme Provider (
@checkstack/ui/ThemeProvider) - Runtime theme management and persistence - Component Adoption - All components use semantic tokens instead of hardcoded colors
How Theme Tokens Work
Section titled “How Theme Tokens Work”HSL Color Space
Section titled “HSL Color Space”All theme tokens are defined using the HSL (Hue, Saturation, Lightness) color space. This provides several advantages:
- Easy manipulation: Adjust brightness, saturation without recalculating RGB values
- Opacity support: Tailwind can inject alpha channels (e.g.,
bg-primary/90) - Consistent dark mode: Adjust lightness values for optimal contrast
CSS Variable Format
Section titled “CSS Variable Format”Theme tokens are stored as raw HSL values without the hsl() wrapper:
:root { --primary: 262 83% 58%; /* Not: hsl(262, 83%, 58%) */}This allows Tailwind to construct the full color value and apply opacity modifiers:
// Tailwind converts this to: rgba(from hsl(262 83% 58%), opacity)className="bg-primary/90"Automatic Dark Mode
Section titled “Automatic Dark Mode”Dark mode is handled via the .dark class applied to the document root:
:root { --background: 0 0% 100%; /* white */}
.dark { --background: 240 10% 4%; /* dark blue-gray */}When the user toggles dark mode, the ThemeProvider adds/removes the .dark class, automatically switching all token values.
Available Theme Tokens
Section titled “Available Theme Tokens”Brand & Actions
Section titled “Brand & Actions”| Token | Purpose | Light Mode | Dark Mode | Usage |
|---|---|---|---|---|
primary | Main brand color, primary actions | Purple 262 83% 58% | Lighter purple 263 70% 65% | Buttons, links, active states |
primary-foreground | Text on primary backgrounds | White 0 0% 100% | White 0 0% 100% | Button text, badge text |
secondary | Secondary actions, less emphasis | Light gray 240 5% 96% | Dark gray 240 4% 16% | Secondary buttons |
secondary-foreground | Text on secondary backgrounds | Dark 240 6% 10% | White 0 0% 100% | Secondary button text |
accent | Highlighted/hover states | Light gray 240 5% 96% | Dark gray 240 4% 16% | Hover backgrounds, menu items |
accent-foreground | Text on accent backgrounds | Dark 240 6% 10% | White 0 0% 100% | Menu text, hover text |
Surfaces & Backgrounds
Section titled “Surfaces & Backgrounds”| Token | Purpose | Light Mode | Dark Mode | Usage |
|---|---|---|---|---|
background | Main page background | White 0 0% 100% | Very dark 240 10% 4% | body, main containers |
foreground | Primary text color | Very dark 240 10% 4% | Off-white 0 0% 98% | Body text, headings |
card | Elevated surface (cards, panels) | White 0 0% 100% | Very dark 240 10% 4% | Card backgrounds |
card-foreground | Text on cards | Very dark 240 10% 4% | Off-white 0 0% 98% | Card text |
popover | Floating elements (dropdowns, tooltips) | White 0 0% 100% | Very dark 240 10% 4% | Dropdown menus, tooltips |
popover-foreground | Text in popovers | Very dark 240 10% 4% | Off-white 0 0% 98% | Dropdown text |
muted | Subtle backgrounds, disabled states | Light gray 240 5% 96% | Dark gray 240 4% 16% | Input backgrounds, disabled |
muted-foreground | Secondary text, placeholders | Medium gray 240 4% 46% | Light gray 240 5% 65% | Placeholders, labels |
Borders & Inputs
Section titled “Borders & Inputs”| Token | Purpose | Light Mode | Dark Mode | Usage |
|---|---|---|---|---|
border | General borders, dividers | Light gray 240 6% 90% | Dark gray 240 4% 16% | Card borders, dividers |
input | Input field borders | Light gray 240 6% 90% | Dark gray 240 4% 16% | Text inputs, selects |
ring | Focus rings, outlines | Purple 262 83% 58% | Lighter purple 263 70% 65% | Focus indicators |
Semantic States
Section titled “Semantic States”| Token | Purpose | Light Mode | Dark Mode | Usage |
|---|---|---|---|---|
destructive | Errors, dangerous actions | Red 0 84% 60% | Darker red 0 63% 50% | Delete buttons, error text on light backgrounds |
destructive-foreground | Text on solid destructive backgrounds | White 0 0% 100% | White 0 0% 100% | Button text, toast text |
success | Success states, confirmations | Green 142 71% 45% | Darker green 142 76% 36% | Success alerts, text on light backgrounds |
success-foreground | Text on solid success backgrounds | White 0 0% 100% | Light green 138 76% 97% | Button text, toast text |
warning | Warnings, caution | Yellow 38 92% 50% | Brighter yellow 48 96% 53% | Warning alerts, text on light backgrounds |
warning-foreground | Text on solid warning backgrounds | Black 0 0% 0% | Yellow-200 48 96% 89% | Button text (black for contrast on yellow) |
info | Informational states | Blue 217 91% 60% | Brighter blue 213 94% 68% | Info alerts, text on light backgrounds |
info-foreground | Text on solid info backgrounds | White 0 0% 100% | Light blue 214 100% 97% | Button text, toast text |
Additional Tokens
Section titled “Additional Tokens”| Token | Purpose | Value |
|---|---|---|
radius | Border radius for rounded corners | 0.5rem (8px) |
chart-1 to chart-5 | Chart/graph colors | Varied palette for data visualization |
When to Use Theme Tokens
Section titled “When to Use Theme Tokens”✅ Always Use Theme Tokens For:
Section titled “✅ Always Use Theme Tokens For:”- Brand Colors: Use
primaryfor your main brand color instead of hardcoded purple/blue - Text Colors: Use
foreground,muted-foreground, never hardcoded grays - Backgrounds: Use
background,card,muted,accentfor surfaces - Borders: Use
borderorinputfor all structural dividers - Interactive States: Use
accentfor hover states,ringfor focus - Semantic Feedback: Use
success,warning,info,destructivefor status indicators
❌ Avoid:
Section titled “❌ Avoid:”- Hardcoded Tailwind Colors: Don’t use
bg-indigo-600,text-gray-500, etc. - Arbitrary Values: Avoid
bg-[#7c3aed]or similar arbitrary colors - Brand-Specific Colors: Don’t hardcode company colors; use semantic tokens
Using Theme Tokens in Custom Components
Section titled “Using Theme Tokens in Custom Components”Basic Usage
Section titled “Basic Usage”Replace hardcoded Tailwind color classes with semantic tokens:
// ❌ Bad: Hardcoded colors<div className="bg-white text-gray-900 border-gray-200"> <button className="bg-indigo-600 text-white hover:bg-indigo-700"> Click me </button></div>
// ✅ Good: Semantic tokens<div className="bg-card text-card-foreground border-border"> <button className="bg-primary text-primary-foreground hover:bg-primary/90"> Click me </button></div>Opacity Modifiers
Section titled “Opacity Modifiers”Leverage Tailwind’s opacity modifiers with theme tokens:
// Subtle backgrounds<div className="bg-primary/10"> {/* 10% opacity primary */}
// Hover states<button className="bg-primary hover:bg-primary/90"> {/* 90% opacity on hover */}
// Borders with transparency<div className="border border-success/30"> {/* 30% opacity success border */}Component Variants
Section titled “Component Variants”Use class-variance-authority (cva) to create semantic variants:
import { cva } from "class-variance-authority";
const buttonVariants = cva( "inline-flex items-center rounded-md font-medium transition-colors", { variants: { variant: { primary: "bg-primary text-primary-foreground hover:bg-primary/90", secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80", destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90", outline: "border border-input bg-transparent hover:bg-accent hover:text-accent-foreground", ghost: "hover:bg-accent hover:text-accent-foreground", }, }, });Semantic Alerts Example
Section titled “Semantic Alerts Example”The Alert component demonstrates the “Smart Theming” pattern for semantic states:
const alertVariants = cva("relative w-full rounded-md border p-4", { variants: { variant: { default: "bg-muted/50 border-border text-foreground", success: "bg-success/10 border-success/30 text-success", warning: "bg-warning/10 border-warning/30 text-warning", error: "bg-destructive/10 border-destructive/30 text-destructive", info: "bg-info/10 border-info/30 text-info", }, },});Key patterns:
- Neutral state: Use
muted,border,foregroundtokens - Semantic backgrounds: Use
{token}/10for subtle backgrounds - Semantic borders: Use
{token}/30for visible but not overwhelming borders - Light background text: Use base tokens like
text-success,text-destructive(NOT-foreground) - Solid background text: Use
-foregroundtokens liketext-destructive-foreground(white/contrasting)
Toggle Component Example
Section titled “Toggle Component Example”The Toggle component shows dynamic state-based theming:
<div className={cn( "relative inline-flex h-6 w-11 items-center rounded-full transition-colors", checked ? "bg-primary" : "bg-input", disabled && "opacity-50 cursor-not-allowed")}> <span className={cn( "inline-block h-4 w-4 transform rounded-full bg-background transition-transform", checked ? "translate-x-6" : "translate-x-1" )} /></div>Common Theming Patterns
Section titled “Common Theming Patterns”Pattern 1: Card with Header
Section titled “Pattern 1: Card with Header”<div className="bg-card text-card-foreground border border-border rounded-lg"> <div className="border-b border-border p-4"> <h2 className="text-foreground font-semibold">Card Title</h2> <p className="text-muted-foreground text-sm">Subtitle or description</p> </div> <div className="p-4"> {/* Card content */} </div></div>Pattern 2: Interactive List Item
Section titled “Pattern 2: Interactive List Item”<button className="w-full text-left p-3 rounded-md hover:bg-accent hover:text-accent-foreground transition-colors"> <h3 className="font-medium">Item Title</h3> <p className="text-sm text-muted-foreground">Item description</p></button>Pattern 3: Form Input
Section titled “Pattern 3: Form Input”<input type="text" className="w-full px-3 py-2 bg-background border border-input rounded-md text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:border-transparent" placeholder="Enter text..."/>Pattern 4: Status Badge
Section titled “Pattern 4: Status Badge”// Success badge<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-success/10 text-success border border-success/30"> Active</span>
// Warning badge<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-warning/10 text-warning border border-warning/30"> Pending</span>Pattern 5: Dropdown Menu
Section titled “Pattern 5: Dropdown Menu”<div className="absolute right-0 mt-2 w-56 rounded-md shadow-lg bg-popover border border-border"> <div className="py-1"> <button className="block w-full text-left px-4 py-2 text-sm text-popover-foreground hover:bg-accent hover:text-accent-foreground"> Menu Item </button> </div></div>Migration Guide: Hardcoded to Semantic
Section titled “Migration Guide: Hardcoded to Semantic”| Old Hardcoded Class | New Semantic Token | Context |
|---|---|---|
bg-indigo-600, bg-blue-600 | bg-primary | Brand/primary buttons |
text-indigo-600 | text-primary | Brand links, icons |
bg-white | bg-background or bg-card | Page backgrounds, cards |
bg-gray-50 | bg-muted/30 or bg-accent/50 | Subtle backgrounds |
bg-gray-100 | bg-muted or bg-accent | Input backgrounds, secondary surfaces |
text-gray-900 | text-foreground | Primary text |
text-gray-600, text-gray-500 | text-muted-foreground | Secondary text, placeholders |
border-gray-200 | border-border | Subtle dividers |
border-gray-300 | border-input | Form field borders |
bg-red-600 | bg-destructive | Delete buttons, errors |
text-red-700, bg-red-50 | text-destructive, bg-destructive/10 | Error text on light backgrounds |
text-green-700, bg-green-50 | text-success, bg-success/10 | Success text on light backgrounds |
text-yellow-600, bg-yellow-50 | text-warning, bg-warning/10 | Warning text on light backgrounds |
text-blue-700, bg-blue-50 | text-info, bg-info/10 | Info text on light backgrounds |
Theme Provider & Runtime Management
Section titled “Theme Provider & Runtime Management”Basic Setup
Section titled “Basic Setup”The ThemeProvider should wrap your application root:
import { ThemeProvider } from "@checkstack/ui";
function App() { return ( <ThemeProvider defaultTheme="system"> <YourApp /> </ThemeProvider> );}Using the Theme Hook
Section titled “Using the Theme Hook”Access and control the current theme:
import { useTheme } from "@checkstack/ui";
function ThemeToggle() { const { theme, setTheme } = useTheme();
return ( <button onClick={() => setTheme(theme === "dark" ? "light" : "dark")}> {theme === "dark" ? "🌙" : "☀️"} </button> );}Theme Persistence
Section titled “Theme Persistence”Theme preferences are automatically persisted to the backend via the theme-backend plugin. When a user changes their theme preference:
- The
ThemeProviderupdates the local state - The
theme-frontendplugin syncs the preference to the backend - The preference is stored in the user’s profile
- The preference syncs across devices and browser sessions
Testing Dark Mode
Section titled “Testing Dark Mode”Always verify your components in both light and dark modes:
- Use the theme toggle in the user menu (bottom-right)
- Check contrast: Ensure text is readable on backgrounds
- Verify semantic colors: Success/warning/error should be distinguishable
- Test interactive states: Hover, focus, active states should be visible
Best Practices
Section titled “Best Practices”- ✅ Use semantic token names (
bg-primary,text-foreground) - ✅ Leverage opacity modifiers (
bg-primary/10,hover:bg-accent/80) - ✅ Test in both light and dark modes
- ✅ Use
@checkstack/uicomponents when possible (already theme-aware) - ✅ Follow the established semantic patterns for your use case
DON’T:
Section titled “DON’T:”- ❌ Use hardcoded Tailwind color classes (
bg-indigo-600) - ❌ Use arbitrary color values (
bg-[#7c3aed]) - ❌ Override theme tokens with inline styles
- ❌ Assume colors will always look the same (dark mode changes them)
- ❌ Use semantic tokens for branding (e.g., don’t use
destructivefor a red brand)
Extending the Theme
Section titled “Extending the Theme”If you need to add new tokens, follow this process:
- Define CSS variables in
core/ui/src/themes.cssfor both:rootand.dark - Map to Tailwind in
tailwind.config.jsundertheme.extend.colors - Document the token in this guide with usage guidelines
- Update components to use the new token where appropriate
Additional Resources
Section titled “Additional Resources”- Tailwind CSS Documentation
- HSL Color Space
- class-variance-authority
- Radix UI - Unstyled components used in
@checkstack/ui