Skip to content

Hue Shifting

This page is explanatory. You should not need to implement hue shifting yourself to use the system.

In the real world, objects rarely stay the same hue as they get lighter or darker.

  • Natural Light: Shadows are often cooler (bluer) due to ambient skylight, while direct highlights are warmer (yellower) from the sun.
  • The Bezold–Brücke Effect: As light intensity increases, our perception of hue shifts. Reds become yellower, and violets become bluer.

If you create a color palette by simply changing lightness (e.g., oklch(0.5 0.2 260)oklch(0.9 0.2 260)), the result can feel “synthetic” or “flat”. The shadows might look muddy, or the highlights might look washed out.

Hue Shifting mimics natural light physics by rotating the hue as lightness changes. This creates palettes that feel:

  1. More Dynamic: Colors feel alive rather than static.
  2. More Natural: Mimics the way light interacts with surfaces in the real world.
  3. More Distinct: Helps differentiate surfaces that are close in lightness.
Static Hue (Boring)

Just changing lightness. The result feels mechanical and flat.

Shifted Hue (Dynamic)

Rotating hue from Blue (260) to Orange (80) as lightness increases. The result feels more vibrant and natural.

While shifting hues is desirable, how we shift them matters. A naive implementation might apply hue rotation linearly (e.g., 1° of rotation for every 1% of lightness).

However, this linear approach often fails to match human perception or design intent:

  1. Drifting Shadows: Deep shadows lose their “cool” base hue too quickly, becoming muddy or ambiguous.
  2. Premature Warmth: Highlights become warm too early in the lightness scale, losing the crispness of the mid-tones.

We need a way to “pin” the hue in the shadows and highlights while concentrating the shift in the mid-tones where it adds the most vibrancy.

To solve this, we use a cubic Bezier curve to map lightness values (0-1) to hue rotation factors (0-1). This creates an “S-curve” that:

  1. Starts Slowly: Keeps the hue stable in the deep shadows.
  2. Accelerates: Concentrates the shift in the mid-tones.
  3. Ends Smoothly: Stabilizes the hue in the bright highlights.

The table below demonstrates a 180° hue shift (Blue → Orange) applied linearly vs. using a Bezier curve. Notice how the Bezier curve preserves the identity of the colors at the extremes.

Linear ShiftBezier Shift (S-Curve)
Dark (10%)
Drifts immediately (+18°)
Stays at base hue (+0°)
Shadow (30%)
Significant shift (+54°)
Preserves cool tones (+15°)
Mid (50%)
Midpoint (+90°)
Midpoint (+90°)
Highlight (70%)
Steady progression (+126°)
Accelerates into warmth (+165°)
Light (90%)
Not fully rotated (+162°)
Finishes rotation (+180°)

Experiment with the Bezier curve control points to see how they affect the hue shift across the lightness spectrum.

Hue Shift Playground

Visualize how hue rotates across the lightness scale.

Lightness (0 → 100)
Rotation (0° → 180°)
Palette Preview
Low Hue at 50% L: 350.0° High

Global Parameters

180°
260°

Curve Values

P1 (0.50, 0.00)
P2 (0.50, 1.00)

Drag the points on the graph to adjust the curve.

The system uses a standard cubic Bezier function where the curve starts at (0,0) and ends at (1,1).

function cubicBezier(t: number, p1: number, p2: number): number {
const oneMinusT = 1 - t;
return (
3 * oneMinusT * oneMinusT * t * p1 + 3 * oneMinusT * t * t * p2 + t * t * t
);
}
export function calculateHueShift(
lightness: number,
config?: HueShiftConfig,
): number {
if (!config) return 0;
const { curve, maxRotation } = config;
const factor = cubicBezier(lightness, curve.p1[1], curve.p2[1]);
return factor * maxRotation;
}

You can customize the curve by adjusting the two control points (p1 and p2) and the maximum rotation angle.

{
"hueShift": {
"curve": {
"p1": [0.5, 0], // Control Point 1 (x, y)
"p2": [0.5, 1] // Control Point 2 (x, y)
},
"maxRotation": 180 // Total degrees of rotation
}
}
  • P1: Controls the “acceleration” out of the shadows. Moving it right delays the shift; moving it up starts the shift earlier.
  • P2: Controls the “deceleration” into the highlights. Moving it left extends the shift; moving it down ends the shift earlier.
  • OKLCH Color Space: Hue rotations happen in the perceptually uniform OKLCH space, ensuring equal visual impact across the spectrum.
  • Chroma Independence: Hue shifts don’t affect saturation, maintaining consistent vibrancy.
  • CSS @property: Registered custom properties allow smooth animated transitions between hue-shifted values.