Tailwind CSS + OKLCH: Best Practices
Learn how to integrate OKLCH colors into your Tailwind CSS workflow. Configure custom colors, use plugins, and build a modern, perceptually uniform design system.
Tailwind CSS + OKLCH: Best Practices
Integrating OKLCH colors into your Tailwind CSS workflow unlocks unprecedented control over your color system. This comprehensive guide shows you how to build production-ready, accessible design systems using perceptually uniform colors.
Why OKLCH + Tailwind?
The combination of Tailwind's utility-first approach and OKLCH's perceptual uniformity creates:
- Consistent Color Scales: Every shade step looks equally spaced
- Predictable Dark Mode: Flip lightness values for automatic dark themes
- Better Accessibility: Easier WCAG compliance through consistent lightness
- Future-Proof Colors: Access to P3 gamut and beyond
Initial Setup
Method 1: Direct OKLCH in Config (Recommended)
The simplest approach - define colors directly in your Tailwind config:
// tailwind.config.js
export default {
theme: {
extend: {
colors: {
primary: {
50: 'oklch(0.97 0.05 250)',
100: 'oklch(0.93 0.08 250)',
200: 'oklch(0.85 0.12 250)',
300: 'oklch(0.75 0.15 250)',
400: 'oklch(0.65 0.18 250)',
500: 'oklch(0.55 0.20 250)', // Base shade
600: 'oklch(0.45 0.20 250)',
700: 'oklch(0.35 0.18 250)',
800: 'oklch(0.25 0.15 250)',
900: 'oklch(0.15 0.10 250)',
}
}
}
}
}
Usage:
<button class="bg-primary-500 hover:bg-primary-600 text-white">
Click me
</button>
Method 2: CSS Variables + Tailwind (Flexible)
For runtime color customization and theme switching:
/* app.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
/* Lightness values */
--l-50: 0.97;
--l-100: 0.93;
--l-500: 0.55;
--l-900: 0.15;
/* Brand hues */
--hue-primary: 250;
--hue-secondary: 340;
/* Primary palette */
--color-primary-50: oklch(var(--l-50) 0.05 var(--hue-primary));
--color-primary-500: oklch(var(--l-500) 0.20 var(--hue-primary));
--color-primary-900: oklch(var(--l-900) 0.10 var(--hue-primary));
}
/* Alternative theme */
.theme-ocean {
--hue-primary: 200; /* Change to teal */
}
}
// tailwind.config.js
export default {
theme: {
extend: {
colors: {
primary: {
50: 'var(--color-primary-50)',
500: 'var(--color-primary-500)',
900: 'var(--color-primary-900)',
}
}
}
}
}
Method 3: TypeScript Configuration (Type-Safe)
For large projects wanting type safety:
// tailwind.config.ts
import type { Config } from 'tailwindcss'
const oklchColor = (l: number, c: number, h: number) =>
`oklch(${l} ${c} ${h})`
const generateScale = (hue: number) => ({
50: oklchColor(0.97, 0.05, hue),
100: oklchColor(0.93, 0.08, hue),
200: oklchColor(0.85, 0.12, hue),
300: oklchColor(0.75, 0.15, hue),
400: oklchColor(0.65, 0.18, hue),
500: oklchColor(0.55, 0.20, hue),
600: oklchColor(0.45, 0.20, hue),
700: oklchColor(0.35, 0.18, hue),
800: oklchColor(0.25, 0.15, hue),
900: oklchColor(0.15, 0.10, hue),
})
export default {
theme: {
colors: {
blue: generateScale(250),
purple: generateScale(300),
pink: generateScale(340),
green: generateScale(145),
yellow: generateScale(85),
red: generateScale(25),
}
}
} satisfies Config
Complete Production Palette
Here's a comprehensive color system for real-world applications:
// tailwind.config.js
export default {
theme: {
colors: {
// Neutrals - slightly cool gray for modern feel
gray: {
50: 'oklch(0.97 0.00 0)',
100: 'oklch(0.93 0.00 0)',
200: 'oklch(0.85 0.01 270)',
300: 'oklch(0.75 0.02 270)',
400: 'oklch(0.65 0.03 270)',
500: 'oklch(0.55 0.04 270)',
600: 'oklch(0.45 0.04 270)',
700: 'oklch(0.35 0.04 270)',
800: 'oklch(0.25 0.03 270)',
900: 'oklch(0.15 0.02 270)',
950: 'oklch(0.10 0.02 270)',
},
// Primary brand color - vibrant blue
blue: {
50: 'oklch(0.97 0.05 250)',
100: 'oklch(0.93 0.08 250)',
200: 'oklch(0.85 0.12 250)',
300: 'oklch(0.75 0.15 250)',
400: 'oklch(0.65 0.18 250)',
500: 'oklch(0.55 0.20 250)',
600: 'oklch(0.45 0.20 250)',
700: 'oklch(0.35 0.18 250)',
800: 'oklch(0.25 0.15 250)',
900: 'oklch(0.15 0.10 250)',
},
// Accent colors
purple: {
50: 'oklch(0.97 0.05 300)',
500: 'oklch(0.60 0.22 300)',
900: 'oklch(0.20 0.15 300)',
},
pink: {
50: 'oklch(0.97 0.05 340)',
500: 'oklch(0.70 0.22 340)',
900: 'oklch(0.25 0.15 340)',
},
// Semantic colors
green: {
50: 'oklch(0.97 0.05 145)',
500: 'oklch(0.60 0.20 145)',
600: 'oklch(0.50 0.20 145)',
700: 'oklch(0.40 0.18 145)',
900: 'oklch(0.20 0.12 145)',
},
yellow: {
50: 'oklch(0.97 0.05 85)',
500: 'oklch(0.75 0.18 85)',
900: 'oklch(0.30 0.12 85)',
},
red: {
50: 'oklch(0.97 0.05 25)',
500: 'oklch(0.55 0.22 25)',
600: 'oklch(0.45 0.22 25)',
700: 'oklch(0.35 0.20 25)',
900: 'oklch(0.20 0.15 25)',
},
// Utility colors
white: 'oklch(1.00 0.00 0)',
black: 'oklch(0.00 0.00 0)',
transparent: 'transparent',
current: 'currentColor',
}
}
}
Dark Mode Implementation
Strategy 1: Automatic Lightness Inversion
// tailwind.config.js
export default {
darkMode: 'class',
theme: {
extend: {
colors: {
// Semantic colors that auto-adjust
background: 'oklch(0.98 0.00 0)',
foreground: 'oklch(0.20 0.02 270)',
muted: 'oklch(0.96 0.01 270)',
'muted-foreground': 'oklch(0.50 0.03 270)',
}
}
}
}
/* Define dark variants */
.dark {
--color-background: oklch(0.12 0.02 270);
--color-foreground: oklch(0.90 0.02 270);
--color-muted: oklch(0.18 0.03 270);
--color-muted-foreground: oklch(0.60 0.04 270);
}
Strategy 2: Explicit Dark Variants
<!-- Light: white background, dark text -->
<!-- Dark: dark background, light text -->
<div class="bg-gray-50 text-gray-900 dark:bg-gray-900 dark:text-gray-50">
Content adapts to theme
</div>
<!-- Interactive elements -->
<button class="bg-blue-600 text-white hover:bg-blue-700
dark:bg-blue-500 dark:hover:bg-blue-400">
Optimized for both modes
</button>
Advanced Custom Utilities
Create powerful OKLCH-based utilities:
// tailwind.config.js
import plugin from 'tailwindcss/plugin'
export default {
plugins: [
// Color manipulation utilities
plugin(function({ addUtilities, theme }) {
const newUtilities = {
// Glow effects using OKLCH alpha
'.glow-sm': {
'box-shadow': '0 0 10px oklch(0.60 0.25 250 / 0.3)',
},
'.glow-md': {
'box-shadow': '0 0 20px oklch(0.60 0.25 250 / 0.5)',
},
'.glow-lg': {
'box-shadow': '0 0 40px oklch(0.60 0.25 250 / 0.6)',
},
// Text emphasis
'.text-emphasis': {
'background': 'linear-gradient(135deg, oklch(0.60 0.25 250), oklch(0.65 0.25 300))',
'-webkit-background-clip': 'text',
'-webkit-text-fill-color': 'transparent',
'background-clip': 'text',
},
// Glass morphism
'.glass': {
'background': 'oklch(0.95 0.02 270 / 0.7)',
'backdrop-filter': 'blur(10px)',
'border': '1px solid oklch(0.90 0.03 270 / 0.3)',
},
}
addUtilities(newUtilities)
}),
// Dynamic color generation
plugin(function({ matchUtilities, theme }) {
matchUtilities(
{
'glow': (value) => ({
'box-shadow': `0 0 20px ${value}`,
}),
},
{ values: theme('colors') }
)
}),
],
}
Usage:
<div class="glow-md">Glowing element</div>
<h1 class="text-emphasis">Gradient text</h1>
<div class="glass p-6">Glassmorphism card</div>
<button class="glow-blue-500/50">Custom glow</button>
Opacity Utilities
OKLCH's alpha channel works seamlessly with Tailwind:
<!-- Background opacity -->
<div class="bg-blue-500/50">50% opacity background</div>
<div class="bg-blue-500/[0.15]">15% opacity background</div>
<!-- Text opacity -->
<p class="text-gray-900/75">75% opacity text</p>
<!-- Border opacity -->
<div class="border-2 border-blue-500/30">Translucent border</div>
<!-- Combining with OKLCH -->
<div class="bg-[oklch(0.55_0.20_250_/_0.3)]">
Direct OKLCH with alpha
</div>
Advanced Gradient System
// tailwind.config.js
export default {
theme: {
extend: {
backgroundImage: {
// OKLCH-based gradients
'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
'gradient-primary': 'linear-gradient(135deg, oklch(0.60 0.25 250), oklch(0.65 0.25 300))',
'gradient-success': 'linear-gradient(135deg, oklch(0.65 0.22 145), oklch(0.70 0.20 165))',
'gradient-sunset': 'linear-gradient(135deg, oklch(0.70 0.25 50), oklch(0.65 0.23 340))',
},
},
},
}
<!-- Gradient backgrounds -->
<div class="bg-gradient-to-r from-blue-500 via-purple-500 to-pink-500">
Rainbow gradient
</div>
<div class="bg-gradient-primary">
Predefined brand gradient
</div>
<!-- Gradient borders -->
<div class="p-0.5 bg-gradient-to-r from-blue-500 to-purple-500 rounded-lg">
<div class="bg-white p-6 rounded-lg">
Gradient border effect
</div>
</div>
Component Examples
Button System
<!-- Primary button -->
<button class="
bg-blue-600 hover:bg-blue-700 active:bg-blue-800
text-white font-medium
px-6 py-2.5 rounded-lg
transition-colors duration-200
shadow-sm hover:shadow-md
">
Primary Action
</button>
<!-- Secondary button -->
<button class="
bg-gray-100 hover:bg-gray-200 active:bg-gray-300
text-gray-900 font-medium
px-6 py-2.5 rounded-lg
transition-colors duration-200
dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-100
">
Secondary Action
</button>
<!-- Outline button -->
<button class="
border-2 border-blue-500 hover:border-blue-600
text-blue-600 hover:text-blue-700 font-medium
px-6 py-2.5 rounded-lg
transition-all duration-200
hover:bg-blue-50
dark:text-blue-400 dark:hover:bg-blue-950
">
Outline Button
</button>
<!-- Ghost button -->
<button class="
text-gray-700 hover:text-gray-900 hover:bg-gray-100
font-medium px-6 py-2.5 rounded-lg
transition-colors duration-200
dark:text-gray-300 dark:hover:text-gray-100 dark:hover:bg-gray-800
">
Ghost Button
</button>
<!-- Destructive button -->
<button class="
bg-red-600 hover:bg-red-700 active:bg-red-800
text-white font-medium
px-6 py-2.5 rounded-lg
transition-colors duration-200
">
Delete
</button>
Card Components
<!-- Basic card -->
<div class="
bg-white dark:bg-gray-900
border border-gray-200 dark:border-gray-800
rounded-xl p-6
shadow-sm hover:shadow-md
transition-shadow duration-300
">
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-2">
Card Title
</h3>
<p class="text-gray-600 dark:text-gray-400">
Card content goes here
</p>
</div>
<!-- Elevated card with accent -->
<div class="
bg-white dark:bg-gray-900
rounded-xl p-6
shadow-lg
border-l-4 border-blue-500
">
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-2">
Featured Content
</h3>
<p class="text-gray-600 dark:text-gray-400">
Important information
</p>
</div>
<!-- Glass card -->
<div class="
bg-white/70 dark:bg-gray-900/70
backdrop-blur-md
border border-gray-200/50 dark:border-gray-700/50
rounded-xl p-6
">
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-2">
Glassmorphism
</h3>
<p class="text-gray-700 dark:text-gray-300">
Modern glass effect
</p>
</div>
Form Elements
<!-- Input field -->
<input
type="text"
class="
w-full px-4 py-2.5
bg-white dark:bg-gray-900
border border-gray-300 dark:border-gray-700
rounded-lg
text-gray-900 dark:text-gray-100
placeholder:text-gray-400 dark:placeholder:text-gray-600
focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent
transition-all duration-200
"
placeholder="Enter text..."
/>
<!-- Select dropdown -->
<select class="
w-full px-4 py-2.5
bg-white dark:bg-gray-900
border border-gray-300 dark:border-gray-700
rounded-lg
text-gray-900 dark:text-gray-100
focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent
transition-all duration-200
">
<option>Option 1</option>
<option>Option 2</option>
</select>
<!-- Checkbox -->
<label class="flex items-center space-x-3 cursor-pointer">
<input
type="checkbox"
class="
w-5 h-5
rounded border-gray-300 dark:border-gray-700
text-blue-600 focus:ring-blue-500 focus:ring-2
transition-colors duration-200
"
/>
<span class="text-gray-700 dark:text-gray-300">
Accept terms
</span>
</label>
Accessibility Best Practices
Contrast Checking
// Ensure adequate contrast using lightness differences
const colors = {
// ✅ Good: 0.55 lightness difference
goodContrast: {
bg: 'oklch(0.15 0.02 270)', // Dark background
text: 'oklch(0.90 0.02 270)', // Light text
},
// ❌ Bad: Only 0.20 lightness difference
badContrast: {
bg: 'oklch(0.50 0.20 250)',
text: 'oklch(0.70 0.15 250)',
},
}
Semantic Color Usage
<!-- Accessible status indicators -->
<div class="flex items-center space-x-2">
<span class="
inline-flex items-center px-3 py-1 rounded-full text-sm font-medium
bg-green-100 text-green-800
dark:bg-green-900 dark:text-green-100
">
Success
</span>
<span class="
inline-flex items-center px-3 py-1 rounded-full text-sm font-medium
bg-yellow-100 text-yellow-800
dark:bg-yellow-900 dark:text-yellow-100
">
Warning
</span>
<span class="
inline-flex items-center px-3 py-1 rounded-full text-sm font-medium
bg-red-100 text-red-800
dark:bg-red-900 dark:text-red-100
">
Error
</span>
</div>
Performance Optimization
Purging Unused Colors
// tailwind.config.js
export default {
content: [
'./src/**/*.{html,js,svelte,ts}',
],
// Only ship colors you actually use
safelist: [
'bg-blue-500',
'text-red-600',
// Add dynamic classes here
],
}
JIT Mode Color Generation
<!-- Generate arbitrary OKLCH values on-the-fly -->
<div class="bg-[oklch(0.75_0.18_145)]">
Custom green shade
</div>
<p class="text-[oklch(0.45_0.22_25)]">
Custom red text
</p>
Migration from Existing Tailwind
// Before - HSL colors
const oldConfig = {
colors: {
primary: {
500: 'hsl(220, 80%, 55%)',
}
}
}
// After - OKLCH colors
const newConfig = {
colors: {
primary: {
500: 'oklch(0.60 0.20 250)', // Equivalent but perceptually uniform
}
}
}
Best Practices Summary
- Use consistent 0.10 lightness steps for predictable scales
- Reduce chroma slightly for very light (>0.90) and very dark (<0.20) shades
- Zero chroma for neutrals to avoid color casts
- Test dark mode by flipping lightness values
- Verify contrast using lightness differences (≥0.40 for AA, ≥0.55 for AAA)
- Leverage CSS variables for runtime theme switching
- Document your system for team consistency
- Use TypeScript for type-safe color generation in large projects
Troubleshooting
Colors look different than expected
- OKLCH is perceptually uniform, not mathematically uniform
- Compare using actual display, not color picker values
- Check if your chroma is within gamut (reduce if colors clip)
Dark mode colors feel wrong
- Don't just invert all values - adjust interactive elements separately
- Reduce chroma slightly in dark mode for less eye strain
- Test on actual displays, not just in DevTools
Performance issues
- Use JIT mode for production builds
- Purge unused colors
- Consider CSS variables for shared values
Conclusion
OKLCH transforms Tailwind CSS from a good utility framework into an exceptional color system. The perceptual uniformity makes creating accessible, beautiful designs effortless while providing predictable results across your entire application.
Next steps: