Hue Shifting
Why Shift Hues?
Section titled “Why Shift Hues?”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:
- More Dynamic: Colors feel alive rather than static.
- More Natural: Mimics the way light interacts with surfaces in the real world.
- More Distinct: Helps differentiate surfaces that are close in lightness.
Just changing lightness. The result feels mechanical and flat.
Rotating hue from Blue (260) to Purple (320) as lightness increases. The result feels more vibrant and natural.
Why Non-Linear Hue Shifting?
Section titled “Why Non-Linear Hue Shifting?”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.
The Problem with Linear Shifting
Section titled “The Problem with Linear Shifting”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.
Shadows can feel “muddy” or flat because they lack the natural coolness of ambient light.
A linear shift (0° to 60°) can feel abrupt in the mid-tones, making the color change too noticeable.
Cubic Bezier Solution
Section titled “Cubic Bezier Solution”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.
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;}Interactive Playground
Section titled “Interactive Playground”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.
Global Parameters
Curve Values
Drag the points on the graph to adjust the curve.
Control Points
Section titled “Control Points”The default configuration uses control points that create an S-curve:
{ "hueShift": { "curve": { "p1": [0.5, 0], "p2": [0.5, 1] }, "maxRotation": 180 }}What These Mean
Section titled “What These Mean”- 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:
- Starts slowly at lightness = 0 (minimal hue shift in darks)
- Accelerates through mid-tones (where our eyes are most sensitive)
- Finishes smoothly at lightness = 1 (full rotation in lights)
Visual Comparison
Section titled “Visual Comparison”Here is how the hue rotation (0° to 180°) is applied across the lightness spectrum (0.1 to 0.9).
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)
Perceptual Benefits
Section titled “Perceptual Benefits”-
Natural Warmth Progression: Mimics how we perceive natural lighting (cool shadows → neutral midtones → warm highlights)
-
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)
-
Smooth Extremes: The flatter curves at 0 and 1 prevent jarring hue jumps in already-extreme lightness values
Customization
Section titled “Customization”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
maxRotationfor 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.
Implementation Note
Section titled “Implementation Note”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.
Related Concepts
Section titled “Related Concepts”- 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