Relative Color Palette
A palette of ten shades cascades from a single CSS custom property — OKLCH lightness steps and color harmonies recompute instantly on every hue change.
You'll get access to the interactive demo with a free account.
This effect is part of Effect.Labs — 811 vanilla effects, some free, some premium. Backgrounds has 50 effects, including 6 free. Explore the category →
3 usage examples



How it works
The effect relies entirely on the --base-hue custom property set on the [data-relcolor] container. All descendants inherit this value via var(--base-hue) inside their oklch() declarations. The first row produces five lightness variants at progressively reduced chroma : oklch(.85 .15 H) (lightest), .75 .20, .60 .25 (base), .45 .20, .30 .15 (darkest). Because OKLCH is perceptually uniform, every lightness step looks visually equivalent regardless of hue — something HSL cannot guarantee.
The second row exposes five chromatic harmonies computed via calc() on the hue : complement at H + 180, analogous at H ± 30, triadic at H + 120 and H + 240. The CSS engine resolves these expressions natively — zero JavaScript computation. Harmony chroma is slightly reduced (0.22 vs 0.25 for the base) so that color-relationship swatches stay distinct without becoming aggressively saturated.
The JavaScript is intentionally minimal : querySelectorAll("[data-relcolor]") initializes each instance with t = 250 (blue-indigo) and sets --base-hue via element.style.setProperty(). On click, t = (t + 47) % 360 advances the hue by 47° — a prime number that visits widely separated angles before cycling back to near-neighbors. Multiple instances on the same page work without conflict because each forEach iteration closes over its own t.
The swatch hover effect (transform: scale(1.2) translateY(-4px), 0.4 s transition) is the component's only animation. Without JavaScript, the palette remains visible thanks to CSS-computed oklch() values — only the click-to-rotate behavior is inactive.
Accessibility
- prefers-reduced-motion: not implemented. The only animation is the hover
scale(1.2)on swatches (0.4 s CSS transition) — a light, user-triggered motion. If this matters in your context, add@media (prefers-reduced-motion: reduce) { .relcolor-swatch { transition: none } }. - The
[data-relcolor]container carries noroleoraria-label— it is treated as decorative. If the component plays a functional role (theme selection), addrole="group"andaria-label="Base color picker". - Keyboard: the container is not focusable (no
tabindex), so hue rotation via click is not keyboard-accessible. For full access, wrap the trigger in a native<button>. - Contrast: OKLCH perceptual uniformity ensures lightness steps are visually distinct from one another, but the text-to-swatch contrast ratio is not computed dynamically — verify it against your target hue before using the component as a functional selector.
- HTML
titleattributes on each swatch ("Lighter", "Light", "Base"…) provide minimal hover labels accessible to assistive technologies.
Browser compatibility
Requires the CSS oklch() function and calc() on custom properties — Baseline 2023, available in all modern browsers since mid-2023.
On older browsers without <code>oklch()</code> support, swatches display a transparent background — no JS errors, layout stays intact. Add a <code>background: #888</code> declaration before each OKLCH rule as a visible fallback for legacy UAs.
The code
HTML structure to paste into your page (CSS + JS available with a premium account):
<div class="relcolor-container" data-relcolor="" style="--base-hue: 250">
<div class="relcolor-base-row">
<div class="relcolor-base"></div>
<span class="relcolor-base-label">Base</span>
</div>
<div class="relcolor-palette">
<div class="relcolor-swatch sw-lighter" title="Lighter"></div>
<div class="relcolor-swatch sw-light" title="Light"></div>
<div class="relcolor-swatch sw-base" title="Base"></div>
<div class="relcolor-swatch sw-dark" title="Dark"></div>
<div class="relcolor-swatch sw-darker" title="Darker"></div>
</div>
<div class="relcolor-palette">
<div class="relcolor-swatch sw-complement" title="Complement"></div>
<div class="relcolor-swatch sw-analogous1" title="Analogous +30"></div>
<div class="relcolor-swatch sw-analogous2" title="Analogous -30"></div>
<div class="relcolor-swatch sw-triadic1" title="Triadic +120"></div>
<div class="relcolor-swatch sw-triadic2" title="Triadic +240"></div>
</div>
<span class="relcolor-label">cliquez pour changer la couleur de base</span>
</div>
Full HTML + CSS + JS, copy-paste ready — with hundreds of premium effects.
Customize
Options passed to the API or data-* attributes:
| Option / property | Default | Effect |
|---|---|---|
| --base-hue (inline style) | 250 | Starting OKLCH hue (0–359). 0 = red, 120 = green, 180 = cyan, 250 = blue-indigo, 300 = violet. Override inline or via JS: el.style.setProperty('--base-hue', 160). |
| t + 47 (JS, line 3) | 47 | Hue step per click. 47 = prime number that avoids near-neighbors. Try 30 (even twelfths) or 60 (six evenly spaced colors — full wheel in 6 clicks). |
| .sw-base { background: oklch(.6 .25 …) } | L=0.6, C=0.25 | Lightness and chroma of the reference swatch. Raise C to 0.37 (sRGB max) for more saturation, lower L to 0.5 for a darker base. |
| .sw-lighter { background: oklch(.85 .15 …) } | L=0.85, C=0.15 | Lightest variant. Keep C below the base value to stay in the sRGB gamut at high lightness. Adjust L between 0.78 and 0.92 for the desired brightness. |
| .relcolor-swatch { width; height; border-radius } | 36px, 36px, 8px | Swatch dimensions and shape. Scale up to 48 × 48 px for documentation display, or down to 24 × 24 px for a compact toolbar picker. |
| .relcolor-base { width; height; border-radius } | 50px, 50px, 12px | Size of the base swatch. Increase to 64 × 64 px for a hero display of the reference color. |
| .relcolor-container { gap; padding; background } | 16px, 20px, #0f0f1a | Row spacing, internal padding, and widget background. Set background: transparent to embed inside an existing card. |
FAQ
--base-hue. In JavaScript, convert your hex to OKLCH with a library like culori: const h = oklch(parse('#3b82f6')).h, then inject the result before the script runs via el.style.setProperty('--base-hue', h).<div class="relcolor-swatch"> elements to the second row with dedicated CSS classes: .sw-split1 { background: oklch(.58 .21 calc(var(--base-hue) + 150)) } and .sw-split2 { background: oklch(.58 .21 calc(var(--base-hue) + 210)) }. No JavaScript changes are needed — new swatches automatically inherit --base-hue.hsl(60 100% 50%) appears far brighter than a blue at hsl(240 100% 50%) at the same lightness value. OKLCH guarantees that oklch(.6 .25 60) and oklch(.6 .25 240) have the same perceived luminance — derived palettes are visually consistent regardless of the chosen hue angle.