Change one perceptual channel when the desired relationship can be named precisely: lightness for brighter or darker, chroma for more or less colorful, and hue for movement around the color wheel. Use mixing, tints, or shades when the goal is a relationship to another endpoint instead.
Match the operation to the visual intent
| Intent | Channel or operation | What stays conceptually fixed |
|---|---|---|
| Make a color brighter or darker | lighten() or darken() |
Oklch chroma and hue |
| Make a color more or less colorful | saturate() or desaturate() |
Oklch lightness and hue |
| Move to another hue family | rotateHue() |
Oklch lightness and chroma |
| Move toward white or black | tint() or shade() |
Nothing is held fixed; colors are mixed |
| Move between two arbitrary colors | Color::mix() |
The chosen interpolation model |
These methods convert through Oklch internally, then return the receiver's concrete color type. The source object remains unchanged.
Compare the three channels
use PhpColor\Color\Color;
$base = Color::parse('#3b82f6');
echo $base->lighten(0.08)->toHex().PHP_EOL;
echo $base->saturate(0.04)->toHex().PHP_EOL;
echo $base->rotateHue(30)->toHex().PHP_EOL;
use PhpColor\Color\Color;
$base = Color::parse('#3b82f6');
echo $base->lighten(0.08)->toHex().PHP_EOL;
echo $base->saturate(0.04)->toHex().PHP_EOL;
echo $base->rotateHue(30)->toHex().PHP_EOL;
The output is:
#549cff
#207dff
#886cee
#549cff
#207dff
#886cee
The three results differ because each operation changes a different coordinate, not because one method is a stronger version of another.
Use relative changes for families, exact channels for targets
lighten() adds to Oklch lightness; darken() subtracts from it:
use PhpColor\Color\Color;
$base = Color::parse('#3b82f6');
$hover = $base->lighten(0.08);
$pressed = $base->darken(0.08);
echo $hover->toHex();
echo $pressed->toHex();
use PhpColor\Color\Color;
$base = Color::parse('#3b82f6');
$hover = $base->lighten(0.08);
$pressed = $base->darken(0.08);
echo $hover->toHex();
echo $pressed->toHex();
Lightness is clamped to 0..1. A negative amount reverses lighten(), and darken($amount) is implemented as lighten(-$amount). Use these methods for relative changes. Convert to Oklch and use withChannel('l', ...) when you require an exact lightness.
Adjust chroma and hue
saturate() changes Oklch chroma, despite its familiar name:
use PhpColor\Color\Color;
$base = Color::parse('#3b82f6');
$vivid = $base->saturate(0.04);
$muted = $base->desaturate(0.08);
$shifted = $base->rotateHue(30);
use PhpColor\Color\Color;
$base = Color::parse('#3b82f6');
$vivid = $base->saturate(0.04);
$muted = $base->desaturate(0.08);
$shifted = $base->rotateHue(30);
saturate() adds the supplied amount and prevents chroma from falling below zero. desaturate() always reduces chroma by the absolute value of its argument. rotateHue() accepts positive or negative degrees and normalizes the result into 0 <= h < 360.
High chroma values can fall outside the gamut of a target RGB space. Inspect the converted output rather than assuming that lightness and chroma survive an RGB round trip unchanged.
Relative channel changes do not guarantee equal contrast steps, semantic state relationships, or in-gamut delivery. For a fixed target, convert to Oklch and use withChannel(); for a reusable progression, build a scale and verify its serialized values.
Continue with Create tints and shades, Derive button states, or Lightness, Chroma, and Hue for visible proofs.