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.
The theming system consists of four key layers:
core/ui/src/themes.css) - Core color definitions using HSL valuestailwind.config.js) - Maps CSS variables to Tailwind utility classes@checkstack/ui/ThemeProvider) - Runtime theme management and persistenceAll theme tokens are defined using the HSL (Hue, Saturation, Lightness) color space. This provides several advantages:
bg-primary/90)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"
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.
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
primary for your main brand color instead of hardcoded purple/blueforeground, muted-foreground, never hardcoded graysbackground, card, muted, accent for surfacesborder or input for all structural dividersaccent for hover states, ring for focussuccess, warning, info, destructive for status indicatorsbg-indigo-600, text-gray-500, etc.bg-[#7c3aed] or similar arbitrary colorsReplace 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>
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 */}
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",
},
},
}
);
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:
muted, border, foreground tokens{token}/10 for subtle backgrounds{token}/30 for visible but not overwhelming borderstext-success, text-destructive (NOT -foreground)-foreground tokens like text-destructive-foreground (white/contrasting)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>
<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>
<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>
<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..."
/>
// 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>
<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>
| 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 |
The ThemeProvider should wrap your application root:
import { ThemeProvider } from "@checkstack/ui";
function App() {
return (
<ThemeProvider defaultTheme="system">
<YourApp />
</ThemeProvider>
);
}
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 preferences are automatically persisted to the backend via the theme-backend plugin. When a user changes their theme preference:
ThemeProvider updates the local statetheme-frontend plugin syncs the preference to the backendAlways verify your components in both light and dark modes:
bg-primary, text-foreground)bg-primary/10, hover:bg-accent/80)@checkstack/ui components when possible (already theme-aware)bg-indigo-600)bg-[#7c3aed])destructive for a red brand)If you need to add new tokens, follow this process:
core/ui/src/themes.css for both :root and .darktailwind.config.js under theme.extend.colors@checkstack/ui