Re: [csswg-drafts] [css-color-4] Clarify that `none` is preserved in calculations (#10211)

The CSS Working Group just discussed ``[css-color-4] Clarify that `none` is preserved in calculations``, and agreed to the following:

* `RESOLVED: split mandatory conversion into (1) Converting powerless components to missing  (2)  Converting to another color space. This either calls 1 at the end, or if the two color spaces are the same, it only calls 1`

<details><summary>The full IRC log of that discussion</summary>
&lt;dholbert> lea: implementations are prematurely converting 'none' to '0' which is lossy and doesn't reflect author intent<br>
&lt;dholbert> lea: the intent of that conversion is to avoid exposing color-space conversion math when you don't have anything better to do<br>
&lt;dholbert> lea: none is converted to the other component when you interpolate<br>
&lt;dholbert> lea: when you do calculations, you preserve 'none' in the same way that you preserve a variable; and then resolve 'none' when it gets output or interpolated<br>
&lt;dholbert> lea: chris added it to agenda because there was a lot of discussion after most recent resolution<br>
&lt;dholbert> lea: my understanding is to preserve 'none' until absolute last moment, when we display or interpolate<br>
&lt;dholbert> ChrisL: you'd proposed splitting it into two steps, one for converting none, one for doing color-space conversions<br>
&lt;dholbert> ChrisL: splitting it up makes it cleaner<br>
&lt;dholbert> lea: I don't remember this, but it sounds reasonable<br>
&lt;dholbert> ChrisL: you proposed it and I agree with it, and it'd make the spec more reasonable<br>
&lt;dholbert> ChrisL: There's something similar in the html spec<br>
&lt;dholbert> ChrisL: 0 hue in oklab is a reddish-purple, and we don't want these colors showing up<br>
&lt;dholbert> lea: "missing component" in that definition is something that contains none?<br>
&lt;dholbert> ChrisL: one is preserving 'none's that were specified<br>
&lt;dholbert> ChrisL: say you have a number for hue, e.g. '60', but the chroma is 0. then it's a missing or powerless component<br>
&lt;dholbert> lea: we use 'none' to represent such components, but also on its own<br>
&lt;dholbert> lea: [gives example]<br>
&lt;ChrisL> s/missing or//<br>
&lt;dholbert> lea: you don't want to replace the whole thing with 'none'; that's against author intent\<br>
&lt;romain> q+<br>
&lt;dholbert> lea: should convert the 'none' (not whole expression) to other component<br>
&lt;astearns> ack lea<br>
&lt;dholbert> romain: this makes sense, in the same color space<br>
&lt;lea> q?<br>
&lt;lea> qq+ to reply<br>
&lt;astearns> ack romain<br>
&lt;dholbert> romain: when there's none in a calc expression and you're interpolating between 2 different color-spaces, then you also have different value ranges<br>
&lt;dholbert> romain: so none+50 means one thing in rgb but another thing in ok[...] color-space<br>
&lt;lea> q-<br>
&lt;dholbert> ChrisL: but we have this concept of analogous components, and those would not be analogoous<br>
&lt;ChrisL> https://drafts.csswg.org/css-color-4/#analogous-components<br>
&lt;dholbert> lea: we're not talking about replacing values wholesale, we're talking about doing math with values.<br>
&lt;dholbert> ChrisL: oklch chroma and CIE ... factor of 100 different<br>
&lt;dholbert> lea: when you're converting between color-spaces, should be treated differently. Good point<br>
&lt;weinig> q+<br>
&lt;dholbert> romain: when same color-space and can be preserved, then it makes sense to do so. When converting to a different color-space, maybe it should become 0<br>
&lt;dholbert> lea: converting to 0 is unfortunate but I can't think of anything better to do<br>
&lt;astearns> ack weinig<br>
&lt;dholbert> weinig: this might be easier if we look at specific examples<br>
&lt;dholbert> weinig: I'm unclear which problem we're discussing<br>
&lt;lea> `linear-gradient(var(--accent-color), oklab(calc(none + .2) none none))`<br>
&lt;dholbert> lea: suppose you have a gradient like this^ ... the second color in that gradient<br>
&lt;romain> color-mix(<br>
&lt;romain>   in srgb<br>
&lt;romain>   rgb(calc(none + 50) 50% 0),<br>
&lt;romain>   color(srgb 0.5 0.5 0),<br>
&lt;dholbert> you can interpolate with any color, and you have a darker color...<br>
&lt;romain> )<br>
&lt;romain> This was my example<br>
&lt;romain> Where `rgb` uses 0-255 but `color(srgb ...)` uses 0-1<br>
&lt;dholbert> lea: you can do this with relative color syntax too<br>
&lt;dholbert> weinig: I didn't think that 'none + .2' is a valid calc expression. doesn't parse at all, because none has no definition in calc<br>
&lt;dholbert> lea: you can end up with it, in relative color syntax<br>
&lt;dholbert> weinig: there's another github issue about how that should work<br>
&lt;dholbert> weinig: what's the problem in this github issue?<br>
&lt;dholbert> lea: we have a resolution to preserve it, whether we end up with 'none' explicitly or via relative colors<br>
&lt;dholbert> ChrisL: and that's not what's implemented. Everyone converts it to 0<br>
&lt;dholbert> lea: and that can become inaccessible<br>
&lt;dholbert> weinig: that's not the intent of the webkit impl; we try to preserve none as long as possible<br>
&lt;dholbert> weinig: maybe WPT has the wrong suggestions here for what is correct?<br>
&lt;dholbert> ChrisL: not sure, I'll put that flag on it<br>
&lt;dholbert> weinig: ignoring 'calc()' which is very complicated and orthogonal here... if you don't have calc, we should be preserving none as much as possible in color-mix<br>
&lt;dholbert> weinig: relative color syntax is a trickier one. We haven't defined how those letters should work with none<br>
&lt;dholbert> romain: this issue is about none in calc, in context of colors<br>
&lt;dholbert> lea: and how you end up with that combination doesn't matter. the question is how do you treat it<br>
&lt;dholbert> weinig: and we resolved that it's preserved in calc?<br>
&lt;dholbert> lea: yes<br>
&lt;dholbert> ChrisL: maybe partly because I didn't put it in the spec, because it was unclear<br>
&lt;dholbert> astearns: so we have the intent from previous resolution, just need clarifications<br>
&lt;dholbert> ChrisL: one of the clarifications was splitting the conversion, -- we now have this phase where you convert missing values to none, and *then* we talk about how to interpolate and convert colors<br>
&lt;dholbert> lea: the issue that romain posted, we don't know how to solve<br>
&lt;dholbert> ChrisL: i agree<br>
&lt;dholbert> weinig: lea, what does none + .2 mean in your example<br>
&lt;ChrisL> "the issue" being different ranges for numerical values<br>
&lt;dholbert> lea: you might have ended up with this type of color with relative color syntax. The intent is "i want to make the color darker"<br>
&lt;dholbert> lea: preserving the 'none' will match that intent<br>
&lt;lea> s/oklab(calc(none + .2) none none)/oklab(calc(none - .2) none none)/<br>
&lt;dholbert> weinig: the idea is to create the oklab with, instead of that calc, just a 'none' where the hue is, and then convert that oklab to whatever the hue is plus .2?<br>
&lt;dholbert> lea: should have been minus .2, sorry<br>
&lt;dholbert> weinig: after the fact replace the none there?<br>
&lt;dholbert> lea: yes<br>
&lt;dholbert> weinig: same if it was oklab? never mind, linear gradients default to oklab<br>
&lt;dholbert> astearns: is there a good definition for that initial split where we convert 0 to none?<br>
&lt;dholbert> ChrisL: yes. Lea said, on April 24, "instead of framing this as color-space conversion, we need 2 algorithms..."<br>
&lt;dholbert> ChrisL: [reads github comment]<br>
&lt;dholbert> ChrisL: that was a comment, not a resolution, but I'd like us to resolve that<br>
&lt;ChrisL> so let us resolve on what lea said, here  https://github.com/w3c/csswg-drafts/issues/10211#issuecomment-2075829784<br>
&lt;dholbert> lea: currently the spec says when your'e converting color spaces that are compatible in some ways, and you have none values, that is preserved. e.g. oklch to lch, with 'none' in the hue, you preserve it<br>
&lt;dholbert> lea: but romain's saying if you're doing *math* with the component, then we can't preserve because the ranges are so different<br>
&lt;dholbert> lea: (e.g. chroma has different ranges in oklch vs lch)<br>
&lt;dholbert> lea: i think we should just say in that case, convert nones to 0<br>
&lt;dholbert> ChrisL: also, authors shouldn't be adding at all in that cases. they should be multiplying<br>
&lt;dholbert> lea: that's a good point... if we say it's converted to 0 when you're doing math, then mutliplying wouldn't work either<br>
&lt;romain> q+<br>
&lt;dholbert> lea: so maybe we should preserve it? and adding will end up with silly results, and that's the author's fault<br>
&lt;astearns> ack romain<br>
&lt;lea> qq+<br>
&lt;dholbert> romain: 3rd option: treat the whole expression as 'none'<br>
&lt;dholbert> not sure if that' sbetter but it's an option<br>
&lt;astearns> ack lea<br>
&lt;Zakim> lea, you wanted to react to romain<br>
&lt;dholbert> lea: this is the worst of all the options we have<br>
&lt;dholbert> lea: e.g. if you want to clamp lightness to a particular range, then you may end up converting the whole expression and losing the clamp<br>
&lt;dholbert> romain: same is true if you convert to 0? e.g. if you try to use +.1 to do a small change in lightness, then you get a giant change if we convert to 0<br>
&lt;lea> `oklch(from var(--color) clamp(0.5, l, 0.6) c h)`<br>
&lt;dholbert> lea: suppose you have this^ color<br>
&lt;dholbert> lea: if you end up with a color that has 'none' as the lightness<br>
&lt;dholbert> if you treat the whole expression as none, you end up with black there<br>
&lt;dholbert> you could end up with anything there. depends on what you interpolate with<br>
&lt;dholbert> if you treat the whole clamp as none, then you have no guarantees about the lightness<br>
&lt;dholbert> lea: the point is to guarantee the range, so you can compose it and ensure a certain level of contrast<br>
&lt;dholbert> romain: I don't disagree, but that's not unique to clamp... if you use a math expression, there are different best and worst cases<br>
&lt;dholbert> romain: for clamp it's bad to make the whole thing none, but for other expressions that might be the best case<br>
&lt;dholbert> romain: but I agree that for clamp, treating the whole thing as none is the worst case<br>
&lt;dholbert> weinig: in this example, where is the 'none' coming from?<br>
&lt;dholbert> lea: from --color<br>
&lt;dholbert> weinig: what might that be?<br>
&lt;dholbert> weinig: like a concrete color<br>
&lt;dholbert> weinig: would an example be `--oklch(none, ...)`<br>
&lt;dholbert> lea: yes<br>
&lt;dholbert> weinig: so if you're going from 'none' to this new thing, and you want to clamp the output to between 5 and 6....<br>
&lt;dholbert> lea: these are coming from different places. there might be design tokens from the web page itself, and you've got tooling that does math with them<br>
&lt;dholbert> lea: if you're trying to write CSS that's agnostic to what colors you get, then [missed]<br>
&lt;dholbert> weinig: how do I interpret what this color means as an engine?<br>
&lt;dholbert> ChrisL: yeah, how do you clamp none to between .5 and .6?<br>
&lt;dholbert> weinig: or is this going to be one color in a bigger expression that we then want to preserve that 'none'... do we need to preserve it until we do interpolation?<br>
&lt;dholbert> lea: that was the spirit of the resolution, yeah<br>
&lt;dholbert> weinig: so if we use your example as a background-color, it would be whatever clamp(0.5, 0, 0.6) would be?<br>
&lt;dholbert> lea: if you have to display it, you have to convert none to 0, so yeah you'd end up with .5 from the clamp expression<br>
&lt;dholbert> weinig: vs. if you interpolate, you'd replace it with the hue on the other side of the interpolation<br>
&lt;dholbert> lea: yes<br>
&lt;dholbert> weinig: got it. it does add a lot of complexity to color representation; we need to add more complexity to our internal representations<br>
&lt;lea> q?<br>
&lt;dholbert> weinig: not a dealbreaker; we already represent calc() deeply like this<br>
&lt;dholbert> lea: I think we have other features that cover this use-case, so if this adds tremendous complexity, it might not be worth it<br>
&lt;dholbert> lea: if implementation complexity is too high for something like this, we could step back and see what else can we do that might be more reasonable. But if it's manageable, I think this is the way to preserve author intent as much as we can<br>
&lt;ChrisL> "The clamp() function takes three calculations—​a minimum value, a central value, and a maximum value—​and represents its central calculation, clamped according to its min and max calculations, favoring the min calculation if it conflicts with the max. "<br>
&lt;ChrisL> So  clamp(0.5, none, 0.6) = 0.5<br>
&lt;dholbert> weinig: I'd have to try to implement it to know for sure<br>
&lt;ChrisL> https://www.w3.org/TR/css-values-4/#comp-func<br>
&lt;ChrisL> q+<br>
&lt;dholbert> weinig: the current system where we use NaN to represent 'none' and keep that 'none' through interpolation is very nice. HAving to replace that with an AST would be unfortunate<br>
&lt;lea> q?<br>
&lt;dholbert> weinig: if there are other ways to do that and preserve that feature for how none can be translated to the system, that'd be great<br>
&lt;dholbert> but we can do it<br>
&lt;astearns> sck ChrisL<br>
&lt;astearns> ack ChrisL<br>
&lt;lea> q+<br>
&lt;astearns> ack lea<br>
&lt;dholbert> ChrisL: clamp() would indeed produce .5 in that scenario discussed above<br>
&lt;dholbert> lea: I'd be much more in favor of converting to 0 prematurely, as compared to making it infectious and making the whole expression none<br>
&lt;dholbert> lea: if preserving it has too high complexity, let's just convert it to 0; that's better than throwing away all the constraints<br>
&lt;dholbert> astearns: resolution?<br>
&lt;dholbert> lea: we could try speccing what we currently have, the resolution we already have<br>
&lt;dholbert> lea: if we get feedback from implementors that this adds to much complexity, then we can revisit and convert to 0<br>
&lt;dholbert> astearns: what do we need to add to preexisting resolution?<br>
&lt;dholbert> ChrisL: I want us to resolve on what lea said, splitting things into 2 stages<br>
&lt;romain> +1 to splitting to two stages<br>
&lt;dholbert> PROPOSED RESOLUTION: split into two stages as lea described in github<br>
&lt;ChrisL> split mandatory conversion into<br>
&lt;ChrisL>     Converting powerless components to missing<br>
&lt;ChrisL>     Converting to another color space. This either calls 1 at the end, or if the two color spaces are the same, it only calls 1.<br>
&lt;dholbert> romain: it'd be good to have some examples that we can run through, too<br>
&lt;dholbert> s/romain/weinig/<br>
&lt;dholbert> weinig: I think in wpt if you try to use none in calc it's a parse error<br>
&lt;dholbert> lea: that's definitely invalid<br>
&lt;dholbert> weinig: explicit none in there is invalid<br>
&lt;dholbert> lea: the intent of relative colors is that you can do conversions without having to worry about internal parse errors<br>
&lt;dholbert> weinig: that's the current grammar<br>
&lt;dholbert> RESOLVED: split mandatory conversion into (1) Converting powerless components to missing  (2)  Converting to another color space. This either calls 1 at the end, or if the two color spaces are the same, it only calls 1<br>
&lt;dholbert> lea: would it make sense to resolve that relative color syntax never results in a parse error, if you already have a valid color?<br>
&lt;dholbert> lea: I guess that's a design principle<br>
</details>


-- 
GitHub Notification of comment by css-meeting-bot
Please view or discuss this issue at https://github.com/w3c/csswg-drafts/issues/10211#issuecomment-3807032366 using your GitHub account


-- 
Sent via github-notify-ml as configured in https://github.com/w3c/github-notify-ml-config

Received on Tuesday, 27 January 2026 19:14:48 UTC