Skip to content

Hue Shifting

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 Purple (320) as lightness increases. The result feels more vibrant and natural.

The color system supports optional hue rotation across the lightness spectrum. This feature allows surfaces to shift from cooler tones in darker regions to warmer tones in lighter regions (or vice versa), creating a more dynamic and perceptually harmonious color palette.

A naive implementation might apply hue rotation linearly. However, this doesn’t match human perception. Our eyes perceive warmth and coolness non-linearly across the lightness spectrum.

No Shift (Static Hue)

Shadows can feel “muddy” or flat because they lack the natural coolness of ambient light.

Linear Shift

A linear shift (0° to 60°) can feel abrupt in the mid-tones, making the color change too noticeable.

Instead, we use a cubic Bezier curve to map lightness values (0-1) to hue rotation factors (0-1). This allows us to keep the hue stable in the deep shadows and bright highlights, while concentrating the shift in the mid-tones where it adds the most vibrancy.

No Shift
Static hue.
Linear Shift
Abrupt mid-tones.
Bezier Shift
Smooth S-curve.
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;
}

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° → 60°)
Result
Hue Only
Black Hue at 50% L: 300.0° White

Global Parameters

60°
270°

Curve Values

P1 (0.33, 0.00)
P2 (0.67, 1.00)

Drag the points on the graph to adjust the curve.

The default configuration uses control points that create an S-curve:

{
"hueShift": {
"curve": {
"p1": [0.5, 0],
"p2": [0.5, 1]
},
"maxRotation": 180
}
}
  • P1: [0.5, 0]: First control point at 50% horizontally, 0% vertically
  • P2: [0.5, 1]: Second control point at 50% horizontally, 100% vertically

This creates a smooth S-curve that:

  1. Starts slowly at lightness = 0 (minimal hue shift in darks)
  2. Accelerates through mid-tones (where our eyes are most sensitive)
  3. Finishes smoothly at lightness = 1 (full rotation in lights)

Here is how the hue rotation (0° to 180°) is applied across the lightness spectrum (0.1 to 0.9).

Linear
Bezier
Dark
Mid
Light

Notice how the Bezier curve:

  • Flattens at the extremes (smoother transitions in very dark/light)
  • Steepens in the middle (more dramatic shift where it matters)
  1. Natural Warmth Progression: Mimics how we perceive natural lighting (cool shadows → neutral midtones → warm highlights)

  2. Better Mid-Tone Separation: The steeper middle section ensures distinct hue differences between closely-spaced surfaces in the mid-lightness range (where most UI elements live)

  3. Smooth Extremes: The flatter curves at 0 and 1 prevent jarring hue jumps in already-extreme lightness values

You can customize the curve by adjusting the control points:

{
"hueShift": {
"curve": {
"p1": [0.3, 0], // Shift acceleration point earlier
"p2": [0.7, 1] // Shift deceleration point later
},
"maxRotation": 120 // Less dramatic overall shift
}
}

Experiment with:

  • Moving P1/P2 horizontally to change where the acceleration happens
  • Moving P1/P2 vertically to create asymmetric curves
  • Adjusting maxRotation for subtler or more dramatic effects

Tip: You can also adjust these settings visually in the Theme Builder. The UI provides a curve editor where you can drag the control points and see the color palette update in real-time.

The cubic Bezier implementation assumes the curve starts at (0,0) and ends at (1,1), with only the middle two control points configurable. This constraint ensures the hue shift is always 0° at lightness 0 and maxRotation at lightness 1, providing predictable behavior while allowing artistic control over the interpolation.

  • 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