You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[css-color-4] Use 0 chroma when hue is powerless during conversion #14133
When performing color space conversion to a cylindrical polar color space, user agents shall treat a hue component as powerless if the chroma (or other measure of colorfulness, such as saturation in hsl) is less than the epsilon (ε) specified for that color space. For example, a gray color converted into oklch() may, due to numerical errors, have an extremely small chroma rather than precisely 0%; as a result, the hue component is powerless.
So hue is powerless when chroma is smaller than 0.000004 but chroma itself is not set to 0.
And any missing component is treated as 0 for the actual conversion math.
So color(from oklch(0.5 0.000004 180) display-p3 r g b) which should have a powerless hue still ends up as: color(display-p3 0.38857936 0.38856994 0.38857244)
Converting that to hsl for example results in color(srgb 0.38858148 0.38856954 0.3885725).
Shouldn't chroma/saturation also be set to 0 when it matches the powerless hue condition? Otherwise the outcome is a color with whatever hue 0deg is for a given color space. This hue might itself be powerless, but when converting to polar spaces it will still surface.
A value of 100 is achromatic; anything greater, I think, is beyond maximum whiteness and blackness, so achromatic as well. You can count powerlessness very close to achromatic or make 100 a hard limit. Either seems like it would work.
Shouldn't chroma/saturation also be set to 0 when it matches the powerless hue condition? Otherwise the outcome is a color with whatever hue 0deg is for a given color space. This hue might itself be powerless, but when converting to polar spaces it will still surface.
I'm not sure the hue matters in this case, because it is...well...powerless. If chroma is very small, the actual chroma shouldn't matter as the resultant hue is powerless regardless. If you force chroma to zero, then you just get whatever random hue your floating-point noise resolves to. Is that better or worse? Some spaces may always return 0 for the hue, but some may return something else.
I've turned off achromatic hues from being normalized to undefined.
If chroma is very small, the actual chroma shouldn't matter as the resultant hue is powerless regardless.
The hue isn't really powerless (as in, it does affect the outcome) when converting to another colorspace and that is kinda the point :)
It will always be set to 0 to make the maths work.
Only when the result also has a powerless hue (destination is polar and chroma/sat fits the epsilon).
Otherwise the result can be a color that is barely but still measurably chromatic.
While setting the chroma to 0 does seem to produce very good results:
The hue isn't really powerless when converting to another colorspace and that is kinda the point :)
It will always be set to 0 to make the maths work.
Yes, if the hue is undefined, then it must be set to something, and 0 is fine, but it could be anything.
Here we normalize hue such that it is undefined and we avoid setting chroma to zero and also set chroma to zero. We get whatever hue floating-point noise gives us:
Otherwise the result can be a color that is barely but still measurably chromatic.
While setting the chroma to 0 does seem to produce very good results:
Yeah, but none of that is visually significant. What this means is that maybe the achromatic threshold for chroma is too large. But, yes, you could force chroma to zero to make it clamp the value to a true achromatic value.
It is true that none of this is visually significant after a single conversion that doesn't do anything else.
But when doing calc expressions in relative color syntax you might suddenly get unexpected results when between zero and the epsilon.
I am thinking of cases where the chroma/saturation of the input color is animated slowly while there is a calc expression that amplifies the chroma/saturation of the output color.
Such a transition would go from:
true achromatic
a hue of 0 but with a significant chroma making that color visible
the actual hue of the input color
With a hard transition between 2 and 3.
While with a forced chroma of 0 it would stay a true achromatic for a bit longer and then go immediately to the expected hue. Although also with a hard transition. There is no way to avoid the discontinuity in such cases, but we can try to make it less surprising.
Yeah, it's possible I can't visualize the case, but I can see colors with chroma on the edge of a threshold teetering back and forth. Normally, if you are operating with a specific color space, this wouldn't be a problem (at least outside of CSS). Maybe there are some peculiarities here with how numbers are converted and being forced to convert back. I guess I would have to see a case to better understand.
I agree that explicitly setting chroma to 0 (rather than leaving it at some very small value) will give greater consistency and avoid amplifying floating-point noise.
@romainmenke would you mind submitting a pull request to explicitly state this in the spec? I agree with the Agenda+ F2F, having concrete text to discuss would be helpful. It looks like color issues are on Thursday morning
When changing a powerless hue component to a [=missing component=],
the chroma (or other measure of colorfulness, such as saturation in ''hsl''; see [=analogous components=]) is set to zero
to avoid amplifying floating-point noise.
For ''hwb'' follow these steps instead:
<pre>
1. if W + B is greater than 99.999 and W + B is less than 100
2. if W and B are not missing
2.1. set B to 100 - W
3. else if W is not missing
3.1. set W to 100
4. else if B is not missing
4.1. set B to 100
</pre>
The CSS Working Group just discussed [css-color-4] Use `0` chroma when hue is powerless during conversion, and agreed to the following:
RESOLVED: Accept text in issue
The full IRC log of that discussion
<TabAtkins> romain: when converting powerless components, powerless hue, to 0
<TabAtkins> romain: this can happen when saturation is 0, or very near
<TabAtkins> romain: when it's converted to another polar space with a significant chroma component, on the other end it's not powerless
<TabAtkins> romain: so proposal is to go ahead and force it to zero
<ChrisL> q+
<TabAtkins> romain: with a special carve out for black because it's not a single channel making it achromatic
<astearns> ack ChrisL
<TabAtkins> ChrisL: I agree, this gets rid of some floating point noise. we have proposed text in the issue
<TabAtkins> JohnJansen: this number doesn't show up in divisions?
<TabAtkins> ChrisL: no
<TabAtkins> TabAtkins: and if it does, 0 chroma happens naturally anyway
<TabAtkins> proposed: Accept text in issue
<TabAtkins> RESOLVED: Accept text in issue
Activity
romainmenke commentedon Jul 4, 2026
It isn't obvious what to do about
hwbwhere powerlessness results from the combined value ofwandb.Currently I am using:
w+bis greater than99.999w+bis less than100wandbare not missing3.1. set
bto100 - wwis not missing4.1. set
wto100bis not missing5.1. set
bto100facelessuser commentedon Jul 6, 2026
I would think when w + b is very close or greater than 1 (or I should say 100 in CSS), you could assume the color is achromatic (hue is powerless). At least, that is what I do in ColorAide: https://github.com/facelessuser/coloraide/blob/main/coloraide/spaces/hwb/__init__.py#L51
A value of 100 is achromatic; anything greater, I think, is beyond maximum whiteness and blackness, so achromatic as well. You can count powerlessness very close to achromatic or make 100 a hard limit. Either seems like it would work.
I'm not sure the hue matters in this case, because it is...well...powerless. If chroma is very small, the actual chroma shouldn't matter as the resultant hue is powerless regardless. If you force chroma to zero, then you just get whatever random hue your floating-point noise resolves to. Is that better or worse? Some spaces may always return 0 for the hue, but some may return something else.
I've turned off achromatic hues from being normalized to undefined.
You just get a different hue that doesn't matter, and affects the round-trip conversion some.
The actual zero percent chroma hue is dictated by whatever noise is in your calculation:
romainmenke commentedon Jul 6, 2026
The hue isn't really powerless (as in, it does affect the outcome) when converting to another colorspace and that is kinda the point :)
It will always be set to
0to make the maths work.Only when the result also has a powerless hue (destination is polar and chroma/sat fits the epsilon).
Otherwise the result can be a color that is barely but still measurably chromatic.
While setting the chroma to
0does seem to produce very good results:csstools/postcss-plugins@af47719#diff-53ef003573657bbe0a2e4a2aba162759c10625269703a2a600e43d8187fa3938R16
romainmenke commentedon Jul 6, 2026
This is a cleaner diff of what the effect is of setting
chroma/satto0: csstools/postcss-plugins@4472cc7#diff-53ef003573657bbe0a2e4a2aba162759c10625269703a2a600e43d8187fa3938facelessuser commentedon Jul 6, 2026
Yes, if the hue is undefined, then it must be set to something, and 0 is fine, but it could be anything.
Here we normalize hue such that it is undefined and we avoid setting chroma to zero and also set chroma to zero. We get whatever hue floating-point noise gives us:
Yeah, but none of that is visually significant. What this means is that maybe the achromatic threshold for chroma is too large. But, yes, you could force chroma to zero to make it clamp the value to a true achromatic value.
romainmenke commentedon Jul 6, 2026
It is true that none of this is visually significant after a single conversion that doesn't do anything else.
But when doing calc expressions in relative color syntax you might suddenly get unexpected results when between zero and the epsilon.
I am thinking of cases where the chroma/saturation of the input color is animated slowly while there is a calc expression that amplifies the chroma/saturation of the output color.
Such a transition would go from:
hueof0but with a significant chroma making that color visibleWith a hard transition between 2 and 3.
While with a forced chroma of
0it would stay a true achromatic for a bit longer and then go immediately to the expected hue. Although also with a hard transition. There is no way to avoid the discontinuity in such cases, but we can try to make it less surprising.facelessuser commentedon Jul 7, 2026
Yeah, it's possible I can't visualize the case, but I can see colors with chroma on the edge of a threshold teetering back and forth. Normally, if you are operating with a specific color space, this wouldn't be a problem (at least outside of CSS). Maybe there are some peculiarities here with how numbers are converted and being forced to convert back. I guess I would have to see a case to better understand.
romainmenke commentedon Jul 7, 2026
Firefox nightly seems to show what I mean with this demo: https://codepen.io/romainmenke/pen/dPNJgaa
Screen.Recording.2026-07-07.at.17.25.06.mov
svgeesus commentedon Jul 31, 2026
I agree that explicitly setting chroma to 0 (rather than leaving it at some very small value) will give greater consistency and avoid amplifying floating-point noise.
@romainmenke would you mind submitting a pull request to explicitly state this in the spec? I agree with the
Agenda+ F2F, having concrete text to discuss would be helpful. It looks like color issues are on Thursday morningromainmenke commentedon Jul 31, 2026
Proposed text:
css-meeting-bot commentedon Aug 6, 2026
The CSS Working Group just discussed
[css-color-4] Use `0` chroma when hue is powerless during conversion, and agreed to the following:RESOLVED: Accept text in issueThe full IRC log of that discussion
<TabAtkins> romain: when converting powerless components, powerless hue, to 0<TabAtkins> romain: this can happen when saturation is 0, or very near
<TabAtkins> romain: when it's converted to another polar space with a significant chroma component, on the other end it's not powerless
<TabAtkins> romain: so proposal is to go ahead and force it to zero
<ChrisL> q+
<TabAtkins> romain: with a special carve out for black because it's not a single channel making it achromatic
<astearns> ack ChrisL
<TabAtkins> ChrisL: I agree, this gets rid of some floating point noise. we have proposed text in the issue
<TabAtkins> JohnJansen: this number doesn't show up in divisions?
<TabAtkins> ChrisL: no
<TabAtkins> TabAtkins: and if it does, 0 chroma happens naturally anyway
<TabAtkins> proposed: Accept text in issue
<TabAtkins> RESOLVED: Accept text in issue
svgeesus commentedon Aug 25, 2026
Closed by #14244