Choose a normalization boundary

Decide when to preserve a parsed color space, convert into working coordinates, and serialize a stable representation for another system.

Keep the parsed color object in its native space until an operation or consumer requires another representation. Convert for a declared purpose, and serialize only at the boundary where a string is required.

Separate four decisions

Normalization is not one automatic conversion. It combines decisions that can occur at different boundaries:

Boundary Decision Useful default
Acceptance Is the text a supported concrete color? Parse into a typed object
Processing Which coordinates suit the operation? Convert only for that operation
Storage or exchange What schema does the consumer require? Preserve the native space when the schema supports it
CSS delivery Which browsers or systems consume the value? Emit the required fallback and wide-gamut value separately

Converting every input to hexadecimal at acceptance combines all four decisions and makes the narrowest representation the default.

Preserve the native space when it matters

Native CSS serialization can retain a supported wide-gamut space across a parse and format round trip:

use PhpColor\Color\Color;

$input = 'color(display-p3 0.92 0.18 0.12 / 0.8)';
$color = Color::parse($input);
$stored = $color->toCss();
$restored = Color::parse($stored);

echo $color::getSpaceName().PHP_EOL;
echo $stored.PHP_EOL;
echo $restored::getSpaceName().PHP_EOL;
echo $restored->getAlpha().PHP_EOL;
use PhpColor\Color\Color; $input = 'color(display-p3 0.92 0.18 0.12 / 0.8)'; $color = Color::parse($input); $stored = $color->toCss(); $restored = Color::parse($stored); echo $color::getSpaceName().PHP_EOL; echo $stored.PHP_EOL; echo $restored::getSpaceName().PHP_EOL; echo $restored->getAlpha().PHP_EOL;

The output is:

display-p3
color(display-p3 0.92 0.18 0.12 / 0.8)
display-p3
0.8
display-p3 color(display-p3 0.92 0.18 0.12 / 0.8) display-p3 0.8

This preserves the space name and the displayed coordinates for this value. It does not establish that every formatted decimal string is a lossless persistence format.

Convert when a consumer defines the target

A consumer may require one fixed schema. Convert explicitly before formatting so the reason for normalization remains visible:

use PhpColor\Color\Color;

$source = Color::parse('oklch(0.65 0.18 264)');
$working = $source->to('oklch');
$cssFallback = $source->to('srgb')->toCss();

echo $working::getSpaceName().PHP_EOL;
echo $cssFallback.PHP_EOL;
use PhpColor\Color\Color; $source = Color::parse('oklch(0.65 0.18 264)'); $working = $source->to('oklch'); $cssFallback = $source->to('srgb')->toCss(); echo $working::getSpaceName().PHP_EOL; echo $cssFallback.PHP_EOL;

The output is:

oklch
rgb(85 136 251)
oklch rgb(85 136 251)

Use Oklch when the next operation needs lightness, chroma, and hue coordinates. Use sRGB when a conventional CSS consumer requires RGB. The target system and operation should choose the representation.

Treat compact formats as deliberate reductions

toHex() always converts through sRGB, clamps channels to the byte range, and rounds them to eight-bit values. A Display P3, Rec. 2020, Lab, or Oklch input therefore cannot retain its original coordinates in hexadecimal.

Native CSS formatters also round decimal output. If stored values must survive a material comparison, serialize them, parse them again, and repeat that comparison on the restored value. Do not assume that an internal result and its displayed string have identical precision.

Keep neighboring decisions in their own guides

This page owns where normalization happens, not the details of every target:

Use a Recipe for a specific import or token-file procedure. Use this decision model when designing the boundary that those tasks implement.

Continue