Brand Colors
How Nightfall preserves your brand identity during theme transformation with strict perceptual budgets.
The Problem
Naive color inversion turns your brand blue into an ugly orange. Even simple lightness flipping can produce muddy, unrecognizable brand colors. Your brand's primary blue at oklch(0.55 0.22 260) inverted naively would become oklch(0.45 0.22 80) — a completely different color.
Nightfall solves this by treating brand colors as a special classification with strict transformation budgets.
Transformation Budgets
Brand colors are constrained by three hard limits:
- Hue: never changes — Your blue stays blue. Period. The hue angle is locked during transformation.
- Chroma: max 15% shift — Saturation may increase slightly to maintain vibrancy on dark backgrounds, or decrease slightly to avoid clipping, but never by more than 15%.
- Lightness: max 20% shift — The color may lighten to maintain contrast on dark backgrounds, but the change is bounded.
- Delta-E: max 15 — The overall perceptual distance between original and transformed brand color must be below 15. This is a safety net that catches any combination of changes that would make the color unrecognizable.
How It Works
function transformBrandColor(original, direction) {
const oklch = toOKLCH(original);
// Hue is LOCKED
const newH = oklch.h;
// Lightness shifts just enough for contrast, within budget
let newL = oklch.l;
if (direction === 'light-to-dark') {
// Lighten brand color for dark backgrounds
newL = Math.min(oklch.l + 0.20, oklch.l * 1.15);
} else {
// Darken brand color for light backgrounds
newL = Math.max(oklch.l - 0.20, oklch.l * 0.85);
}
// Chroma adjusts slightly for contrast
let newC = oklch.c;
newC = clamp(newC, oklch.c * 0.85, oklch.c * 1.15);
// Verify delta-E budget
const result = { l: newL, c: newC, h: newH };
const distance = deltaE(oklch, result);
if (distance > 15) {
// Pull back toward original until within budget
return interpolate(oklch, result, 15 / distance);
}
return result;
}Example Transformations
/* Brand blue — light to dark */
--brand-primary: oklch(0.55 0.22 260) /* Original: #2563eb */;
--brand-primary: oklch(0.62 0.20 260) /* Dark: #3b82f6 */;
/* ΔL: +0.07 | ΔC: -0.02 | ΔH: 0 | ΔE: 4.2 ✓ */
/* Brand purple — light to dark */
--brand-accent: oklch(0.50 0.25 300) /* Original: #7c3aed */;
--brand-accent: oklch(0.58 0.23 300) /* Dark: #8b5cf6 */;
/* ΔL: +0.08 | ΔC: -0.02 | ΔH: 0 | ΔE: 5.1 ✓ */
/* Brand green — light to dark */
--brand-success: oklch(0.55 0.18 145) /* Original: #16a34a */;
--brand-success: oklch(0.62 0.17 145) /* Dark: #22c55e */;
/* ΔL: +0.07 | ΔC: -0.01 | ΔH: 0 | ΔE: 3.8 ✓ */Registering Brand Colors
Nightfall auto-detects brand colors (saturated, non-gray colors used on interactive elements). You can also register them explicitly for tighter control:
module.exports = {
brand: {
// Colors to preserve (hex, rgb, oklch, or CSS variable names)
preserve: ['#2563eb', '#7c3aed', '#16a34a'],
// Optional: tighten or loosen budgets
maxChromaShift: 0.15, // default: 0.15 (15%)
maxLightnessShift: 0.20, // default: 0.20 (20%)
maxDeltaE: 15, // default: 15
}
};Delta-E Scale
For context, here is what delta-E values mean perceptually:
- 0-1 — Not perceptible to the human eye
- 1-2 — Perceptible only through close observation
- 2-5 — Perceptible at a glance
- 5-10 — Clearly different but still recognizably the same color
- 10-15 — Different shade of the same hue family
- 15+ — Starts to feel like a different color entirely
Nightfall's default budget of 15 keeps brand colors firmly within the "same color family" range.