tailwind-oklch
v0.5.0
Published
OKLCH color composition system for Tailwind CSS v4
Readme
tailwind-oklch
An okLCH color composition system for Tailwind CSS. Uses independent CSS variables for luminance, chroma, and hue, so you can use atomic utility classes to modify just the luminance for emphasis (with no opacity hacks), just the chroma for decoration, just the hue for theme or semantic meanings.
Note: This is a working concept; it's not really in production anywhere; YMMV.
Why okLCH?
okLCH (Lightness, Chroma, Hue) is a perceptually uniform color space. Unlike HSL, colors at the same lightness and chroma look equally bright regardless of hue. This makes it possible to build systematic, predictable color palettes from simple numeric scales instead of hand-picking individual hex values.
tailwind-oklch takes this further: instead of defining dozens of static color tokens, you compose colors on the fly from three axes. CSS custom property inheritance means a parent can set a hue and children automatically share it — override just the axis you need.
Installation
pnpm add tailwind-oklchSetup
In your main CSS file, import both the core CSS and the shorthand plugin:
@import "tailwindcss";
@import "tailwind-oklch";
@plugin "tailwind-oklch/plugin";Core Concepts
The Three Axes
Every color is built from three independent pieces:
| Axis | What it controls | Example values |
|---|---|---|
| Luminance Contrast (LC) | How far from the page color, on a 0–10 scale | 0–10, base, fore |
| Chroma (C) | Colorfulness / saturation | lo, mlo, mid, mhi, hi |
| Hue (H) | Color identity | primary, accent, success, warning, danger, info, neutral |
Luminance Contrast Scale
The 0–10 scale measures contrast with the page — not absolute lightness:
- 0 /
base= close to the page color (blends in) - 10 /
fore= high contrast with the page (stands out, like text) - 1–9 = evenly distributed between those endpoints
This means bg-lc-3 is always "3 steps from the page" — a subtle, low-contrast element in either light or dark mode.
CSS Cascade Inheritance
Every utility both sets its axis variable and applies the resolved oklch() color. Sensible defaults are provided at :root, so a single class like bg-lc-5 immediately produces a visible color. Variables inherit down the DOM, so a parent's hue automatically flows to children.
Usage
Decomposed Utilities (single-axis control)
Set one axis at a time. The other two axes inherit from the parent or the root defaults.
| Pattern | Sets | Example |
|---|---|---|
| bg-lc-{L} | background luminance contrast | bg-lc-5, bg-lc-base, bg-lc-fore |
| bg-c-{C} | background chroma | bg-c-lo, bg-c-mid, bg-c-hi |
| bg-h-{H} | background hue | bg-h-primary, bg-h-accent, bg-h-danger |
| text-lc-{L} | text luminance contrast | text-lc-fore, text-lc-8 |
| text-c-{C} | text chroma | text-c-mid |
| text-h-{H} | text hue | text-h-accent |
| border-lc-{L} | border luminance contrast | border-lc-3 |
| border-c-{C} | border chroma | border-c-mlo |
| border-h-{H} | border hue | border-h-neutral |
The same pattern applies to border-b-* (border-bottom), accent-*, from-* (gradient from), to-* (gradient to), and shadow-*.
Global Hue and Chroma
Most of the time, every color property on an element shares the same hue — the differences are in lightness and chroma. The hue-* utility sets the hue for all color properties at once:
<!-- Set hue once, vary L and C per property -->
<div class="hue-danger bg-1-mid text-10-lo border-3-mhi">
<button class="bg-5-mhi text-0-lo">Acknowledge</button>
<button class="bg-5-mhi text-10-lo">Cancel</button>
</div>chroma-* does the same for chroma:
<!-- Everything low-chroma -->
<div class="hue-primary chroma-lo bg-lc-1 text-lc-fore border-lc-3">Per-property utilities (bg-h-*, text-c-*, etc.) still work as overrides when you need one property to differ.
Shorthand Utilities
The plugin generates shorthands for common combinations:
Two-axis: {property}-{L}-{C} — sets luminance and chroma, inherits hue from the cascade (set by hue-* or :root default):
<div class="hue-accent bg-3-mhi text-10-lo border-2-mid">Three-axis: {property}-{L}-{C}-{H} — sets all three axes explicitly in a single class:
<div class="bg-3-mhi-accent text-fore-lo-neutral border-5-mid-primary">Available properties: bg, text, border, border-b, accent, from, to.
Combining Decomposed and Shorthand
The real power comes from combining both. Set a full color on a parent, then override a single axis on children:
<!-- Parent sets the full color context -->
<div class="bg-3-mhi-accent text-fore-lo-accent">
<!-- Child lightens only the background on hover -->
<button class="hover:bg-lc-6">Lighter on hover</button>
<!-- Child drops to page-level luminance, inherits chroma + hue -->
<footer class="bg-lc-base">Same accent hue, page-level brightness</footer>
<!-- Child switches to a different hue, keeps luminance + chroma -->
<aside class="bg-h-success">Success-colored sidebar</aside>
</div>Using with Tailwind Modifiers
All utilities work with standard Tailwind modifiers:
<button class="bg-3-mid-primary hover:bg-lc-5 focus:bg-lc-6">
Hover and focus states
</button>
<div class="bg-lc-base dark:bg-lc-1">
Responsive to color scheme
</div>
<input class="border-lc-3 focus:border-c-mid focus:border-h-primary">
Border chroma increases on focus
</input>Relative Luminance Offsets
Sometimes you don't want to set an absolute luminance — you want to nudge it relative to the inherited value. The lc-up and lc-down utilities shift luminance toward more contrast or toward less contrast without replacing the underlying --bg-l or --tx-l variable. This means children still inherit the original value.
lc-up-{N}— increase contrast (move away from the page color)lc-down-{N}— decrease contrast (move toward the page color)
Where {N} is 1–5, with each step equal to ~0.08 OKLCH lightness (roughly one position on the 0–10 scale).
Available for bg and text:
| Pattern | Effect |
|---|---|
| bg-lc-up-{N} | Background becomes more contrasting |
| bg-lc-down-{N} | Background becomes less contrasting |
| text-lc-up-{N} | Text becomes more contrasting |
| text-lc-down-{N} | Text becomes less contrasting |
The direction automatically adapts to light/dark mode — "up" always means more contrast with the page, "down" always means less, regardless of whether luminance values are increasing or decreasing.
<!-- A card with a hover state one step brighter/darker than the parent -->
<div class="bg-3-mlo-primary">
<button class="hover:bg-lc-up-1">Slightly more contrast on hover</button>
<span class="bg-lc-down-2">Subtler background, closer to page</span>
</div>
<!-- Muted secondary text that's two steps less contrasting than default -->
<p class="text-fore-lo-neutral">
Primary text
<span class="text-lc-down-2">Secondary text</span>
</p>Gradients
<div class="bg-gradient-to-r from-3-mid-primary to-3-mid-accent">
Gradient from primary to accent
</div>
<!-- Or decomposed: override just the hue on the "to" end -->
<div class="bg-gradient-to-r from-3-mid-primary to-h-accent">
Same luminance and chroma, different hue
</div>Customization
Custom Hues
Override the default hue values in a @theme block:
@theme {
--hue-primary: 180; /* teal */
--hue-accent: 320; /* pink */
}Default hue values:
| Name | Default | Color |
|---|---|---|
| primary | 233 | blue/indigo |
| accent | 350 | red/pink |
| success | 145 | green |
| warning | 55 | yellow |
| danger | 15 | orange-red |
| info | 220 | blue |
| neutral | 260 | purple-gray |
Custom Luminance Contrast Range
Shift the overall luminance contrast endpoints:
@theme {
--lc-range-start: 0.15; /* base (0) is darker in dark mode */
--lc-range-end: 0.95; /* fore (10) is brighter in dark mode */
}Runtime Theming
Because everything is driven by CSS custom properties, you can re-theme the entire app at runtime:
// Switch the primary hue to teal
document.documentElement.style.setProperty('--hue-primary', '180');Reference
Luminance Contrast Scale
| Stop | Dark Mode | Light Mode |
|---|---|---|
| 0 / base | 0.12 | 0.95 |
| 1 | 0.20 | 0.87 |
| 2 | 0.28 | 0.79 |
| 3 | 0.36 | 0.71 |
| 4 | 0.44 | 0.63 |
| 5 | 0.52 | 0.55 |
| 6 | 0.60 | 0.47 |
| 7 | 0.68 | 0.39 |
| 8 | 0.76 | 0.31 |
| 9 | 0.84 | 0.23 |
| 10 / fore | 0.92 | 0.15 |
Named Chroma Stops
| Name | Value | Description |
|---|---|---|
| lo | 0.02 | Near-neutral, subtle tint |
| mlo | 0.06 | Low saturation |
| mid | 0.12 | Medium saturation |
| mhi | 0.18 | Vivid |
| hi | 0.25 | Maximum saturation |
A numeric chroma scale (c-10 through c-95) is also available for finer control in the decomposed utilities.
LC Adjustment Steps
Used by the relative luminance offset utilities (bg-lc-up-*, bg-lc-down-*, etc.):
| Step | OKLCH L offset | Approximate scale positions |
|---|---|---|
| 1 | 0.08 | ~1 step |
| 2 | 0.16 | ~2 steps |
| 3 | 0.24 | ~3 steps |
| 4 | 0.32 | ~4 steps |
| 5 | 0.40 | ~5 steps |
Override in a @theme block:
@theme {
--lc-adj-1: 0.06; /* smaller steps */
--lc-adj-2: 0.12;
}Supported Properties
Global context setters (set all properties at once):
| Utility | Sets |
|---|---|
| hue-{H} | Hue for all properties (--bg-h, --tx-h, --bd-h, etc.) |
| chroma-{C} | Chroma for all properties (--bg-c, --tx-c, --bd-c, etc.) |
Per-property utilities:
| Prefix | CSS Property | Decomposed | 2-axis Shorthand | 3-axis Shorthand |
|---|---|---|---|---|
| bg | background-color | bg-lc-*, bg-c-*, bg-h-* | bg-{L}-{C} | bg-{L}-{C}-{H} |
| text | color | text-lc-*, text-c-*, text-h-* | text-{L}-{C} | text-{L}-{C}-{H} |
| border | border-color | border-lc-*, border-c-*, border-h-* | border-{L}-{C} | border-{L}-{C}-{H} |
| border-b | border-bottom-color | border-b-lc-*, border-b-c-*, border-b-h-* | border-b-{L}-{C} | border-b-{L}-{C}-{H} |
| accent | accent-color | accent-lc-*, accent-c-*, accent-h-* | accent-{L}-{C} | accent-{L}-{C}-{H} |
| from | gradient from | from-lc-*, from-c-*, from-h-* | from-{L}-{C} | from-{L}-{C}-{H} |
| to | gradient to | to-lc-*, to-c-*, to-h-* | to-{L}-{C} | to-{L}-{C}-{H} |
| shadow | shadow color | shadow-lc-*, shadow-c-*, shadow-h-* | — | — |
Light / Dark Mode
Dark mode is the default. Light mode activates when the root element does not have the .dark class (:root:not(.dark)). The luminance contrast scale flips automatically — lc-0 is always near the page, lc-10 is always high contrast — no additional classes needed.
License
MIT
