549 lines
12 KiB
Markdown
549 lines
12 KiB
Markdown
# Theme Token System
|
|
|
|
## Overview
|
|
|
|
This document describes the CSS custom property-based design token system used for Tailwind + shadcn/ui theming.
|
|
|
|
## Token Philosophy
|
|
|
|
### Semantic Naming
|
|
Tokens describe **purpose**, not appearance:
|
|
- [OK] `--primary` (describes role)
|
|
- [ERROR] `--blue-600` (describes appearance)
|
|
|
|
### Benefits
|
|
- Easy theme switching
|
|
- Consistent design system
|
|
- Automatic dark mode support
|
|
- Forward-compatible with Tailwind v4
|
|
|
|
## Color Token Structure
|
|
|
|
### HSL Format
|
|
|
|
All colors use HSL (Hue, Saturation, Lightness) format without the `hsl()` wrapper:
|
|
|
|
```css
|
|
/* Define as space-separated values */
|
|
--primary: 222.2 47.4% 11.2%;
|
|
|
|
/* Use with hsl() wrapper */
|
|
background-color: hsl(var(--primary));
|
|
|
|
/* With alpha transparency */
|
|
background-color: hsl(var(--primary) / 0.5);
|
|
```
|
|
|
|
### Why HSL?
|
|
- **Intuitive**: Easier to adjust than RGB
|
|
- **Lightness control**: Simple to create shades
|
|
- **Alpha transparency**: Works with modern CSS syntax
|
|
- **Tooling**: Better design tool support
|
|
|
|
## Core Color Tokens
|
|
|
|
### Base Colors
|
|
|
|
```css
|
|
:root {
|
|
/* Background & Foreground */
|
|
--background: 0 0% 100%; /* Pure white */
|
|
--foreground: 222.2 84% 4.9%; /* Near black text */
|
|
|
|
/* Card (elevated surfaces) */
|
|
--card: 0 0% 100%; /* White card background */
|
|
--card-foreground: 222.2 84% 4.9%; /* Card text */
|
|
|
|
/* Popover (floating UI) */
|
|
--popover: 0 0% 100%; /* White popover */
|
|
--popover-foreground: 222.2 84% 4.9%; /* Popover text */
|
|
}
|
|
```
|
|
|
|
### Semantic Colors
|
|
|
|
```css
|
|
:root {
|
|
/* Primary (brand color, main actions) */
|
|
--primary: 222.2 47.4% 11.2%;
|
|
--primary-foreground: 210 40% 98%;
|
|
|
|
/* Secondary (less prominent actions) */
|
|
--secondary: 210 40% 96.1%;
|
|
--secondary-foreground: 222.2 47.4% 11.2%;
|
|
|
|
/* Muted (disabled states, subtle backgrounds) */
|
|
--muted: 210 40% 96.1%;
|
|
--muted-foreground: 215.4 16.3% 46.9%;
|
|
|
|
/* Accent (highlights, hover states) */
|
|
--accent: 210 40% 96.1%;
|
|
--accent-foreground: 222.2 47.4% 11.2%;
|
|
|
|
/* Destructive (errors, delete actions) */
|
|
--destructive: 0 84.2% 60.2%;
|
|
--destructive-foreground: 210 40% 98%;
|
|
}
|
|
```
|
|
|
|
### UI Element Colors
|
|
|
|
```css
|
|
:root {
|
|
/* Borders */
|
|
--border: 214.3 31.8% 91.4%; /* Subtle border */
|
|
--input: 214.3 31.8% 91.4%; /* Input border */
|
|
|
|
/* Focus & Selection */
|
|
--ring: 222.2 84% 4.9%; /* Focus ring */
|
|
|
|
/* Status Colors (optional) */
|
|
--success: 142 76% 36%;
|
|
--success-foreground: 0 0% 100%;
|
|
--warning: 38 92% 50%;
|
|
--warning-foreground: 0 0% 100%;
|
|
--info: 199 89% 48%;
|
|
--info-foreground: 0 0% 100%;
|
|
}
|
|
```
|
|
|
|
## Dark Mode Tokens
|
|
|
|
### Dark Mode Strategy
|
|
|
|
Use `.dark` class to override tokens:
|
|
|
|
```css
|
|
.dark {
|
|
/* Base */
|
|
--background: 222.2 84% 4.9%; /* Near black */
|
|
--foreground: 210 40% 98%; /* Near white */
|
|
|
|
/* Card */
|
|
--card: 222.2 84% 4.9%;
|
|
--card-foreground: 210 40% 98%;
|
|
|
|
/* Primary */
|
|
--primary: 210 40% 98%;
|
|
--primary-foreground: 222.2 47.4% 11.2%;
|
|
|
|
/* Secondary */
|
|
--secondary: 217.2 32.6% 17.5%;
|
|
--secondary-foreground: 210 40% 98%;
|
|
|
|
/* Muted */
|
|
--muted: 217.2 32.6% 17.5%;
|
|
--muted-foreground: 215 20.2% 65.1%;
|
|
|
|
/* Accent */
|
|
--accent: 217.2 32.6% 17.5%;
|
|
--accent-foreground: 210 40% 98%;
|
|
|
|
/* Destructive */
|
|
--destructive: 0 62.8% 30.6%;
|
|
--destructive-foreground: 210 40% 98%;
|
|
|
|
/* Borders */
|
|
--border: 217.2 32.6% 17.5%;
|
|
--input: 217.2 32.6% 17.5%;
|
|
|
|
/* Focus */
|
|
--ring: 212.7 26.8% 83.9%;
|
|
}
|
|
```
|
|
|
|
## Theme Presets
|
|
|
|
### Zinc (Default)
|
|
|
|
Cool, neutral gray tones. Professional and versatile.
|
|
|
|
```css
|
|
:root {
|
|
--background: 0 0% 100%;
|
|
--foreground: 240 10% 3.9%;
|
|
--primary: 240 5.9% 10%;
|
|
--primary-foreground: 0 0% 98%;
|
|
--muted: 240 4.8% 95.9%;
|
|
--muted-foreground: 240 3.8% 46.1%;
|
|
--border: 240 5.9% 90%;
|
|
}
|
|
|
|
.dark {
|
|
--background: 240 10% 3.9%;
|
|
--foreground: 0 0% 98%;
|
|
--primary: 0 0% 98%;
|
|
--primary-foreground: 240 5.9% 10%;
|
|
--muted: 240 3.7% 15.9%;
|
|
--muted-foreground: 240 5% 64.9%;
|
|
--border: 240 3.7% 15.9%;
|
|
}
|
|
```
|
|
|
|
### Slate
|
|
|
|
Slightly cooler than Zinc. Tech-focused feel.
|
|
|
|
```css
|
|
:root {
|
|
--background: 0 0% 100%;
|
|
--foreground: 222.2 84% 4.9%;
|
|
--primary: 222.2 47.4% 11.2%;
|
|
--primary-foreground: 210 40% 98%;
|
|
--muted: 210 40% 96.1%;
|
|
--muted-foreground: 215.4 16.3% 46.9%;
|
|
--border: 214.3 31.8% 91.4%;
|
|
}
|
|
|
|
.dark {
|
|
--background: 222.2 84% 4.9%;
|
|
--foreground: 210 40% 98%;
|
|
--primary: 210 40% 98%;
|
|
--primary-foreground: 222.2 47.4% 11.2%;
|
|
--muted: 217.2 32.6% 17.5%;
|
|
--muted-foreground: 215 20.2% 65.1%;
|
|
--border: 217.2 32.6% 17.5%;
|
|
}
|
|
```
|
|
|
|
### Neutral
|
|
|
|
True neutral grays. Clean and minimal.
|
|
|
|
```css
|
|
:root {
|
|
--background: 0 0% 100%;
|
|
--foreground: 0 0% 3.9%;
|
|
--primary: 0 0% 9%;
|
|
--primary-foreground: 0 0% 98%;
|
|
--muted: 0 0% 96.1%;
|
|
--muted-foreground: 0 0% 45.1%;
|
|
--border: 0 0% 89.8%;
|
|
}
|
|
|
|
.dark {
|
|
--background: 0 0% 3.9%;
|
|
--foreground: 0 0% 98%;
|
|
--primary: 0 0% 98%;
|
|
--primary-foreground: 0 0% 9%;
|
|
--muted: 0 0% 14.9%;
|
|
--muted-foreground: 0 0% 63.9%;
|
|
--border: 0 0% 14.9%;
|
|
}
|
|
```
|
|
|
|
## Spacing & Sizing Tokens
|
|
|
|
### Border Radius
|
|
|
|
```css
|
|
:root {
|
|
--radius: 0.5rem; /* Base radius: 8px */
|
|
}
|
|
|
|
/* Usage in Tailwind config */
|
|
borderRadius: {
|
|
lg: 'var(--radius)',
|
|
md: 'calc(var(--radius) - 2px)',
|
|
sm: 'calc(var(--radius) - 4px)',
|
|
}
|
|
```
|
|
|
|
### Font Family (Optional)
|
|
|
|
```css
|
|
:root {
|
|
--font-sans: ui-sans-serif, system-ui, sans-serif;
|
|
--font-mono: ui-monospace, 'Cascadia Code', monospace;
|
|
}
|
|
```
|
|
|
|
## Usage in Tailwind Config
|
|
|
|
### Extending Theme
|
|
|
|
```ts
|
|
// tailwind.config.ts
|
|
export default {
|
|
theme: {
|
|
extend: {
|
|
colors: {
|
|
border: 'hsl(var(--border))',
|
|
input: 'hsl(var(--input))',
|
|
ring: 'hsl(var(--ring))',
|
|
background: 'hsl(var(--background))',
|
|
foreground: 'hsl(var(--foreground))',
|
|
primary: {
|
|
DEFAULT: 'hsl(var(--primary))',
|
|
foreground: 'hsl(var(--primary-foreground))'
|
|
},
|
|
secondary: {
|
|
DEFAULT: 'hsl(var(--secondary))',
|
|
foreground: 'hsl(var(--secondary-foreground))'
|
|
},
|
|
destructive: {
|
|
DEFAULT: 'hsl(var(--destructive))',
|
|
foreground: 'hsl(var(--destructive-foreground))'
|
|
},
|
|
muted: {
|
|
DEFAULT: 'hsl(var(--muted))',
|
|
foreground: 'hsl(var(--muted-foreground))'
|
|
},
|
|
accent: {
|
|
DEFAULT: 'hsl(var(--accent))',
|
|
foreground: 'hsl(var(--accent-foreground))'
|
|
},
|
|
popover: {
|
|
DEFAULT: 'hsl(var(--popover))',
|
|
foreground: 'hsl(var(--popover-foreground))'
|
|
},
|
|
card: {
|
|
DEFAULT: 'hsl(var(--card))',
|
|
foreground: 'hsl(var(--card-foreground))'
|
|
}
|
|
},
|
|
borderRadius: {
|
|
lg: 'var(--radius)',
|
|
md: 'calc(var(--radius) - 2px)',
|
|
sm: 'calc(var(--radius) - 4px)'
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
## Using Tokens in Components
|
|
|
|
### In CSS
|
|
|
|
```css
|
|
.custom-component {
|
|
background-color: hsl(var(--background));
|
|
color: hsl(var(--foreground));
|
|
border: 1px solid hsl(var(--border));
|
|
border-radius: var(--radius);
|
|
}
|
|
|
|
/* With alpha */
|
|
.overlay {
|
|
background-color: hsl(var(--background) / 0.8);
|
|
}
|
|
```
|
|
|
|
### With Tailwind Utilities
|
|
|
|
```tsx
|
|
<div className="bg-background text-foreground border-border">
|
|
<h1 className="text-primary">Heading</h1>
|
|
<p className="text-muted-foreground">Subtitle</p>
|
|
<Button variant="destructive">Delete</Button>
|
|
</div>
|
|
```
|
|
|
|
## Customization Guide
|
|
|
|
### Changing Primary Color
|
|
|
|
1. **Find your HSL values**: Use a color picker that shows HSL
|
|
2. **Update light mode**:
|
|
```css
|
|
:root {
|
|
--primary: 270 80% 45%; /* Purple example */
|
|
--primary-foreground: 0 0% 100%; /* White text */
|
|
}
|
|
```
|
|
3. **Update dark mode**:
|
|
```css
|
|
.dark {
|
|
--primary: 270 80% 65%; /* Lighter purple */
|
|
--primary-foreground: 240 10% 10%; /* Dark text */
|
|
}
|
|
```
|
|
4. **Test contrast**: Use WebAIM contrast checker
|
|
|
|
### Creating Custom Tokens
|
|
|
|
```css
|
|
/* Add to globals.css */
|
|
:root {
|
|
/* Custom tokens */
|
|
--sidebar-width: 16rem;
|
|
--header-height: 4rem;
|
|
--brand-gradient: linear-gradient(135deg, hsl(var(--primary)), hsl(var(--accent)));
|
|
}
|
|
|
|
/* Use in components */
|
|
.sidebar {
|
|
width: var(--sidebar-width);
|
|
}
|
|
```
|
|
|
|
### Adding More Semantic Colors
|
|
|
|
```css
|
|
:root {
|
|
--success: 142 76% 36%;
|
|
--success-foreground: 0 0% 100%;
|
|
--warning: 38 92% 50%;
|
|
--warning-foreground: 0 0% 100%;
|
|
}
|
|
|
|
.dark {
|
|
--success: 142 71% 45%;
|
|
--warning: 38 92% 60%;
|
|
}
|
|
```
|
|
|
|
```ts
|
|
// Add to tailwind.config.ts
|
|
colors: {
|
|
success: {
|
|
DEFAULT: 'hsl(var(--success))',
|
|
foreground: 'hsl(var(--success-foreground))'
|
|
},
|
|
warning: {
|
|
DEFAULT: 'hsl(var(--warning))',
|
|
foreground: 'hsl(var(--warning-foreground))'
|
|
}
|
|
}
|
|
```
|
|
|
|
## Best Practices
|
|
|
|
### 1. Always Pair Background/Foreground
|
|
|
|
```tsx
|
|
[OK] Good
|
|
<div className="bg-primary text-primary-foreground">
|
|
Readable text
|
|
</div>
|
|
|
|
[ERROR] Bad
|
|
<div className="bg-primary text-foreground">
|
|
Low contrast
|
|
</div>
|
|
```
|
|
|
|
### 2. Test Both Themes
|
|
|
|
Always verify colors in both light and dark mode:
|
|
```bash
|
|
# Use browser DevTools to toggle:
|
|
document.documentElement.classList.toggle('dark')
|
|
```
|
|
|
|
### 3. Use Semantic Tokens
|
|
|
|
```tsx
|
|
[OK] Good (semantic)
|
|
<p className="text-muted-foreground">Helper text</p>
|
|
<Button variant="destructive">Delete</Button>
|
|
|
|
[ERROR] Bad (hard-coded)
|
|
<p className="text-gray-500 dark:text-gray-400">Helper text</p>
|
|
<Button className="bg-red-600">Delete</Button>
|
|
```
|
|
|
|
### 4. Maintain Contrast Ratios
|
|
|
|
- Normal text: ≥ 4.5:1
|
|
- Large text: ≥ 3:1
|
|
- UI components: ≥ 3:1
|
|
|
|
## Tools for Theme Development
|
|
|
|
### Color Pickers with HSL
|
|
- [HSL Color Picker](https://hslpicker.com/)
|
|
- [Coolors](https://coolors.co/)
|
|
- [Palettte App](https://palettte.app/)
|
|
|
|
### Contrast Checkers
|
|
- [WebAIM Contrast Checker](https://webaim.org/resources/contrastchecker/)
|
|
- [Contrast Ratio](https://contrast-ratio.com/)
|
|
- Chrome DevTools (Lighthouse)
|
|
|
|
### Theme Generators
|
|
- [Realtime Colors](https://realtimecolors.com/)
|
|
- [shadcn/ui Themes](https://ui.shadcn.com/themes)
|
|
- [Tailwind Color Generator](https://uicolors.app/create)
|
|
|
|
## Example: Complete Theme
|
|
|
|
```css
|
|
/* globals.css */
|
|
@tailwind base;
|
|
@tailwind components;
|
|
@tailwind utilities;
|
|
|
|
@layer base {
|
|
:root {
|
|
/* Base */
|
|
--background: 0 0% 100%;
|
|
--foreground: 240 10% 3.9%;
|
|
|
|
/* UI Elements */
|
|
--card: 0 0% 100%;
|
|
--card-foreground: 240 10% 3.9%;
|
|
--popover: 0 0% 100%;
|
|
--popover-foreground: 240 10% 3.9%;
|
|
|
|
/* Primary Brand */
|
|
--primary: 240 5.9% 10%;
|
|
--primary-foreground: 0 0% 98%;
|
|
|
|
/* Secondary Actions */
|
|
--secondary: 240 4.8% 95.9%;
|
|
--secondary-foreground: 240 5.9% 10%;
|
|
|
|
/* Muted Elements */
|
|
--muted: 240 4.8% 95.9%;
|
|
--muted-foreground: 240 3.8% 46.1%;
|
|
|
|
/* Accents */
|
|
--accent: 240 4.8% 95.9%;
|
|
--accent-foreground: 240 5.9% 10%;
|
|
|
|
/* Destructive */
|
|
--destructive: 0 84.2% 60.2%;
|
|
--destructive-foreground: 0 0% 98%;
|
|
|
|
/* Borders & Inputs */
|
|
--border: 240 5.9% 90%;
|
|
--input: 240 5.9% 90%;
|
|
--ring: 240 5.9% 10%;
|
|
|
|
/* Radius */
|
|
--radius: 0.5rem;
|
|
}
|
|
|
|
.dark {
|
|
--background: 240 10% 3.9%;
|
|
--foreground: 0 0% 98%;
|
|
--card: 240 10% 3.9%;
|
|
--card-foreground: 0 0% 98%;
|
|
--popover: 240 10% 3.9%;
|
|
--popover-foreground: 0 0% 98%;
|
|
--primary: 0 0% 98%;
|
|
--primary-foreground: 240 5.9% 10%;
|
|
--secondary: 240 3.7% 15.9%;
|
|
--secondary-foreground: 0 0% 98%;
|
|
--muted: 240 3.7% 15.9%;
|
|
--muted-foreground: 240 5% 64.9%;
|
|
--accent: 240 3.7% 15.9%;
|
|
--accent-foreground: 0 0% 98%;
|
|
--destructive: 0 62.8% 30.6%;
|
|
--destructive-foreground: 0 0% 98%;
|
|
--border: 240 3.7% 15.9%;
|
|
--input: 240 3.7% 15.9%;
|
|
--ring: 240 4.9% 83.9%;
|
|
}
|
|
}
|
|
```
|
|
|
|
## Resources
|
|
|
|
- **HSL Color Space**: https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/hsl
|
|
- **shadcn/ui Theming**: https://ui.shadcn.com/docs/theming
|
|
- **Tailwind CSS Customization**: https://tailwindcss.com/docs/customizing-colors
|
|
- **Design Tokens**: https://www.w3.org/community/design-tokens/
|