Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[css-images-4] Clarifying CSS gradient rendering for edge cases involving "longer hue" interpolation #11381

Open
jfkthame opened this issue Dec 16, 2024 · 19 comments

Comments

@jfkthame
Copy link
Contributor

jfkthame commented Dec 16, 2024

I am seeing differences between Firefox, Chrome and Safari regarding how gradients with the "longer hue" interpolation method are rendered. I think I understand what the spec expects, but none of the browsers tested get this right, so I'd like to confirm my interpretation — and perhaps some clarification or additional examples may be needed in the spec.

Testcase: https://codepen.io/jfkthame/pen/azopZOx

(Note that there have been some recent patches to gradient rendering in Firefox. The description below refers to current Nightly builds; older versions have some different and less self-consistent results.)

This renders a number of 100px-wide gradient swatches, all of them defined as linear-gradient(to right in hsl longer hue, ...) with two color stops, red and blue, at a variety of positions. (The red and blue tick-marks above and below each swatch indicate the positions of the color stops defining the gradient.)

Results I'm seeing in Firefox Nightly, Chrome Canary, and Safari Tech Preview:
image

The simplest case is (a), with the red stop at the left edge and the blue stop at the right. No issues here: we simply render a gradient from red to blue, going "the long way around" the HSL wheel.

What happens if the first and last stops defining the gradient do not cover the full extent of the area to be rendered? In (b), the blue stop is in the middle, so what happens in the right-hand half of the swatch? According to https://drafts.csswg.org/css-images-4/#coloring-gradient-line,

Before the first color stop, the gradient line is the color of the first color stop, and after the last color stop, the gradient line is the color of the last color stop.

which seems to imply that the right-hand half of (b) should be solid blue. However, all three tested browsers actually paint a further gradient here, with a full cycle of the HSL wheel from the blue stop at 50px all the way around to blue again at the 100px edge. I presume the browsers are adding an "implicit" blue color stop at the right-hand edge, and then performing a longer-hue interpolation between the actual blue stop at 50px and that implicit one. I don't see any justification in the spec for this behavior: I think it's a bug, but given that all three browsers behave this way, maybe I'm missing something?

Example (c) is similar: here, both the red and blue color stops are moved in from the edges of the swatch, so I think the expected rendering should have 20px of solid red at the left, and 25px of blue at the right, with the longer-hue gradient from red to blue in between. But all three browsers also render a complete 360° gradient in both the left and right portions, outside the defined color stops. Bug?

(Aside: the description of linear-gradient on MDN https://developer.mozilla.org/en-US/docs/Web/CSS/gradient/linear-gradient

By default, if there is no color with a 0% stop, the first color declared will be at that point. Similarly, the last color will continue to the 100% mark, or be at the 100% mark if no length has been declared on that last stop.

may be a bit misleading here. That first sentence might be understood to imply creating an extra stop at 0%, duplicating the first declared color, rather than simply painting that color from its stop position back to 0%; and analogously, although the wording is less ambiguous, an implementer might think they should duplicate the last declared color at 100%. And that would indeed result in the observed behavior. But if my understanding of the actually-expected behavior is correct, I'll suggest rewording this for better clarity.)

Interestingly, in example (d), where the blue stop is moved beyond the right-hand end of the swatch, Chrome and Safari suddenly switch their behavior and now do not use a gradient in the area to the left of the red stop: they turn this into solid color. (Firefox, on the other hand, continues to apply a gradient there, just like in the preceding examples.) I think the Chrome/Safari behavior here is what the spec requires, but I don't understand why they only do this correctly once the blue color stop is beyond the end of the rendered area.

Example (e) shows the equivalent case with the red stop placed beyond the left edge: now, Chrome and Safari render a solid color for the area beyond the blue stop, while Firefox still paints a gradient there.

Examples (f) and (g) serve to illustrate the abrupt change in behavior in Chrome and Safari. The two stops are just shifted right by 1px in (g) compared to (f). In Firefox, this just results in a barely-perceptible shift in the gradient; but in Chrome and Safari it drastically changes the result.

QUESTION 1: Am I right in thinking that any area before the first color stop or after the last color stop should simply render as a solid color, or is longer-hue interpolation expected to somehow "project" into these areas (as the browsers currently do, though not entirely consistently)?

The second group of swatches, (h) through (l), have the two color stops at the exact same position. My reading of the spec is that the correct rendering would have solid red to the left of the stop position, and solid blue to the right. But all three browsers actually render a complete 360° gradient: to the right of the color stops, they render blue-to-blue (all the way around the hue circle), and to the left they render red-to-red (all the way around). For (h) through (j), this result is consistent across all the browsers, and appears to result from adding "implicit stops" at the left and right edges of the swatch, duplicating the color of the nearest real stop.

In example (k), the position of the red and blue stops has been moved beyond the right-hand edge of the swatch. In Firefox, this "stretches" the gradient to the right; it still begins with red at the left edge, but the visible gradient no longer cycles all the way back to red because the stop is outside the swatch area. In Chrome and Safari, on the other hand, the end of the 360° gradient seems to be "clamped" to the right-hand side of the swatch, so (k) looks identical to (j). I can't see any justification for that behavior — though I think the browsers are wrong to be rendering a gradient at all in this case.

Finally, in (l) the stops have been moved before the left-hand edge of the swatch. Again, Firefox stretches the gradient; Chrome "clamps" the left edge, so that (l) is identical to (h). Safari renders this example the same as (j) and (k), which I think is a bug.

QUESTION 2: Is a longer-hue linear gradient with stops at a single position a special case that should render some kind of gradient (as all browsers currently do, regardless of where the stops are positioned), or is that just an implementation bug?

(Note that there are a couple of existing WPT tests that expect this behavior; e.g. see http://wpt.live/css/css-images/gradient/gradient-single-stop-longer-hue-hsl.html. I'm suggesting this may be wrong. Does the web depend on it?)

@jfkthame
Copy link
Contributor Author

To follow up, here is what I think my codepen example ought to look like, by my reading of the spec:
image

Currently, none of the three tested browsers produces this rendering. So if my understanding is correct, and assuming we don't want to modify the spec in the light of what browsers are actually doing, then all three engines have bugs here.

@jfkthame
Copy link
Contributor Author

It occurs to me that it should be possible to emulate a longer hue gradient using shorter hue (which browsers generally don't seem to have issues rendering) by inserting an extra color stop in the middle to "force" the interpolation to go the long way around the hue circle. We can use color-mix to create this stop.

I made a copy of my codepen example using this, and this does indeed render all the examples as I expected(*).

I'm feeling increasingly sure that the current behavior we see of "projecting" the longer hue gradient beyond the first and last stops that define it is a bug in all three of the engines; and that creating a gradient of any kind when the color stops are both (or all) at the same position is likewise a bug. (It's the same bug, really: just that the extent of the actual gradient has shrunk to zero, and all that remains is the continuation of the starting and ending colors.)

So unless I'm missing some extra part of the spec that supports these behaviors, I'm intending to file bugs against the browsers and create some WPT tests to check the behavior.

(*) Except that Safari renders swatch (l) in solid red instead of blue, as mentioned at the end of the original description. That is surely a webkit bug.

@schenney-chromium
Copy link
Contributor

I would be very very worried about changing the appearance of sites that have come to depend on the current behavior, though maybe not many do. Is it possible to craft a query against HTTPArchive that looks for the gradient syntax that you are testing (i.e. gradient styles that hit the conditions of interest).

I would try to change the spec rather than browsers if this turns out to be something that has significant usage. Even though this does look to me like some kind of browser problem with the treatment of identical or near-identical stop colors.

@jfkthame
Copy link
Contributor Author

It's not "identical or near-identical stop colors" that are at issue here; it's what browsers do with longer hue gradient interpolation when the first and last defined color stops are not at the 0% and 100% positions. This is an issue whether the gradient is between completely different colors — such as red and blue in my examples — or from a single color all the way around the hue circle back to the same color, as in the gradient-single-stop-longer-hue test.

@jfkthame
Copy link
Contributor Author

I would try to change the spec rather than browsers

I'd be more ready to be persuaded by this if (a) the existing behavior was fully interoperable between browsers (but it's not, see screenshot in the original issue); and (b) the existing behavior of Blink and Webkit had some clear internal consistency, which I would suggest it lacks.

In what model does it make sense that

data:text/html,<div style="width:100px;height:300px;
         background:linear-gradient(in hsl longer hue,red 250px,blue 300px);">

and

data:text/html,<div style="width:100px;height:300px;
         background:linear-gradient(in hsl longer hue,red 250px,blue 301px);">

have the radically different renderings that Chrome and Safari currently give them?
image

Surely the only difference between

data:text/html,<div style="width:100px;height:300px;
         background:linear-gradient(in hsl shorter hue,red 50px,blue 250px);">

and

data:text/html,<div style="width:100px;height:300px;
         background:linear-gradient(in hsl longer hue,red 50px,blue 250px);">

should be in the region between y=50px and 250px, where the first example takes the "short" arc around the HSL color wheel from red through magenta to blue, and the second example takes the "long" arc via orange, yellow, green, cyan. But there's no logical reason to change what happens outside the bounds of that region, as currently happens:
image

@jfkthame
Copy link
Contributor Author

jfkthame commented Jan 4, 2025

I have gone ahead and filed bugs against all three engines, as ISTM their behavior is a clear violation of the spec, and doesn't really make any sense from an author's point of view either.

@LeaVerou
Copy link
Member

LeaVerou commented Jan 4, 2025

I wonder if this Chromium bug that my apprentice @DmitrySharabin recently found could also be relevant.

@jfkthame
Copy link
Contributor Author

jfkthame commented Jan 4, 2025

That looks like an unrelated color-mixing/interpolation bug, I think.

moz-wptsync-bot pushed a commit to web-platform-tests/wpt that referenced this issue Jan 6, 2025
The new tests here are reftest versions of the test swatches from
w3c/csswg-drafts#11381.

The references for existing "single-stop-longer" tests are also changed,
because they were based on a faulty interpretation; the spec does not
call for any kind of gradient to be extrapolated beyond the first and
last defined color stop positions, so a "single stop" gradient actually
renders a single solid color.

Differential Revision: https://phabricator.services.mozilla.com/D233217

bugzilla-url: https://bugzilla.mozilla.org/show_bug.cgi?id=1939948
gecko-commit: fcdb3710b9837f0de8fd19cfe533f9fba8d010d0
gecko-reviewers: longsonr
moz-v2v-gh pushed a commit to mozilla/gecko-dev that referenced this issue Jan 7, 2025
…. r=longsonr

The new tests here are reftest versions of the test swatches from
w3c/csswg-drafts#11381.

The references for existing "single-stop-longer" tests are also changed,
because they were based on a faulty interpretation; the spec does not
call for any kind of gradient to be extrapolated beyond the first and
last defined color stop positions, so a "single stop" gradient actually
renders a single solid color.

Differential Revision: https://phabricator.services.mozilla.com/D233217
moz-wptsync-bot pushed a commit to web-platform-tests/wpt that referenced this issue Jan 7, 2025
The new tests here are reftest versions of the test swatches from
w3c/csswg-drafts#11381.

The references for existing "single-stop-longer" tests are also changed,
because they were based on a faulty interpretation; the spec does not
call for any kind of gradient to be extrapolated beyond the first and
last defined color stop positions, so a "single stop" gradient actually
renders a single solid color.

Differential Revision: https://phabricator.services.mozilla.com/D233217

bugzilla-url: https://bugzilla.mozilla.org/show_bug.cgi?id=1939948
gecko-commit: fcdb3710b9837f0de8fd19cfe533f9fba8d010d0
gecko-reviewers: longsonr
i3roly pushed a commit to i3roly/firefox-dynasty that referenced this issue Jan 8, 2025
…. r=longsonr

The new tests here are reftest versions of the test swatches from
w3c/csswg-drafts#11381.

The references for existing "single-stop-longer" tests are also changed,
because they were based on a faulty interpretation; the spec does not
call for any kind of gradient to be extrapolated beyond the first and
last defined color stop positions, so a "single stop" gradient actually
renders a single solid color.

Differential Revision: https://phabricator.services.mozilla.com/D233217
sadym-chromium pushed a commit to web-platform-tests/wpt that referenced this issue Jan 14, 2025
The new tests here are reftest versions of the test swatches from
w3c/csswg-drafts#11381.

The references for existing "single-stop-longer" tests are also changed,
because they were based on a faulty interpretation; the spec does not
call for any kind of gradient to be extrapolated beyond the first and
last defined color stop positions, so a "single stop" gradient actually
renders a single solid color.

Differential Revision: https://phabricator.services.mozilla.com/D233217

bugzilla-url: https://bugzilla.mozilla.org/show_bug.cgi?id=1939948
gecko-commit: fcdb3710b9837f0de8fd19cfe533f9fba8d010d0
gecko-reviewers: longsonr
@yisibl
Copy link
Contributor

yisibl commented Feb 12, 2025

I'm worried that this will break the compatibility of the site.

Currently, there are many places that use longer hue to create a rainbow gradient.

.foo {
  background: linear-gradient(to right in hsl longer hue, red 0 0);
}

With this proposed change, this fails to produce a gradient and will only show up as a red background.

This breaks compatibility with existing sites and CSS authors must change the CSS above to:

.foo {
  background: linear-gradient(to right in hsl longer hue, red 0 100%);
 /* or */
  background: linear-gradient(to right in hsl longer hue, red, red);
}

So, can we relax the restriction so that a gradient can be generated when there is only one color stop.

cc @LeaVerou

@mysteryDate is implementing HueInterpolationMethod in Canvas in Chrome, this also requires consensus. WDYT?

@tabatkins
Copy link
Member

That example isn't one color stop, tho - it's two color stops (two positions with one color, defining two stops).

You say "many sites" are relying on this to generate rainbows, but I don't actually see rainbows very much in practice on the web. Is this just a cute CSS trick that has gotten some shares, or do we have evidence it's actually used in a non-trivial number of sites (and, ideally, have evidence that rendering them as a solid color would actually harm the rendering, rather than just change the rendering)?

That said, I'm not strongly opposed to specifying that we always add one additional color stop at the beginning/end of the list, placed either at 0%/100% or on top of the existing first/last color stop if they're beyond the 0-100% range, and with the same color as the first/last stop. If this does indeed look to be web-required, that's acceptable, but I'd prefer to avoid it if we can, and just fix the browser bugs.


As an aside, we should also probably think about exactly what behavior we do want between a double-position stop like that. Double-position stops were added to make it easy to generate solid-color strips in a gradient without having to repeat your color; I suspect that people might expect that to remain true even if they're doing longer-hue interpolation otherwise.

@jfkthame
Copy link
Contributor Author

As an aside, we should also probably think about exactly what behavior we do want between a double-position stop like that. Double-position stops were added to make it easy to generate solid-color strips in a gradient without having to repeat your color; I suspect that people might expect that to remain true even if they're doing longer-hue interpolation otherwise.

Hmm, yes, I suppose that might take people by surprise.

The spec text currently says that:

A color stop with two positions is equivalent to specifying two color stops with the same color, one for each position.

which means that linear-gradient(in hsl longer hue, red 0% 100%) should be exactly equivalent to linear-gradient(in hsl longer hue, red 0%, red 100%) (or indeed to linear-gradient(in hsl longer hue, red, red)). I'd be a bit reluctant to break that equivalence by giving a "double-position stop" some kind of special handling, distinct from two separately-defined stops with the same color.

The spec goes on to mention that:

Specifying two locations makes it easier to create solid-color "stripes" in a gradient, without having to repeat the color twice.

Perhaps a note should be added here clarifying that this does not apply to longer hue gradients, because it is inherent in longer-hue gradients that between one stop and a following stop of the same color, there will be a complete "cycle" of color rather than a band of solid color.

@jfkthame
Copy link
Contributor Author

You say "many sites" are relying on this to generate rainbows, but I don't actually see rainbows very much in practice on the web. Is this just a cute CSS trick that has gotten some shares, or do we have evidence it's actually used in a non-trivial number of sites (and, ideally, have evidence that rendering them as a solid color would actually harm the rendering, rather than just change the rendering)?

FWIW, we fixed this gradient-rendering behavior in Firefox 136 (currently in Beta), so that the regions before the first and after the last color stops now render solid colors, not an "extra" full-cycle gradient. This also means, of course, that linear-gradient(in hsl longer hue, red 0 0) no longer generates a "rainbow", it is just solid red.

While it's still early days (this has only recently reached the Beta channel, and won't reach full Release until early March), we have not yet seen any breakage as a result. It's true that there are "demos" such as @yisibl mentioned which will no longer work, but it should be simple for their authors to change to the correct form of gradient definition: just use <color> 0 100% instead of <color> 0 0.

I'm pretty skeptical that such "rainbow gradients generated from a single position" are something the web really depends on to a significant degree. I would much prefer to see browsers fix their rendering bugs rather than enshrine this illogical quirk as a special case in the spec.

@tabatkins
Copy link
Member

I'd be a bit reluctant to break that equivalence by giving a "double-position stop" some kind of special handling, distinct from two separately-defined stops with the same color.

Yeah, on further thought I think I'd prefer to avoid complicating this, too. The note just needs an amendment, as you say.

@yisibl
Copy link
Contributor

yisibl commented Feb 14, 2025

Perhaps a note should be added here clarifying that this does not apply to longer hue gradients, because it is inherent in longer-hue gradients that between one stop and a following stop of the same color, there will be a complete "cycle" of color rather than a band of solid color.

I'm definitely in favor of it! Also, hopefully some examples can be added for rainbow gradients to avoid more misunderstandings.

@yisibl
Copy link
Contributor

yisibl commented Feb 14, 2025

After rethinking, I also agree that it would be better for browsers to fix it, which prevents the specification from getting complicated and reduces the learning cost for users.


I'm worried that this will break the compatibility of the site.
Currently, there are many places that use longer hue to create a rainbow gradient.

.foo {
 background: linear-gradient(to right in hsl longer hue, red 0 0);
}

I raised the concern that it could lead to broken compatibility in #issuecomment-2652590989 above. I've done more research for this and this is the initial conclusion.

Looking back, since all three browsers implemented the specification incorrectly. This led to a misunderstanding by many CSS authors that in hsl longer hue, red 0 0 could produce gradients.

Thankfully, this feature may not be used by many people at the moment, and it's not too late.

@CGQAQ and I will be working on fixing this in Chrome(See patch 6247246). And possibly add a Chrome UseCounters to count exactly how many sites are affected. If necessary, we can issue a warning in the console ⚠️.

In addition, I've collected some data to make it easier to assess the impact.

Potentially Affected Sites

Here are some real use cases found through GitHub searches:

Site CSS Source Code Link Screenshot
https://dreameduinfo.com/
@raselmahmud-coder
conic-gradient( from var(--bg-angle) in oklch longer hue, oklch(0.85 0.37 0) 0 0 ) open Image
https://leanrada.com/htmz
@Kalabasa
linear-gradient(135deg in hsl longer hue, red, red 50%) open Image

Affected front-end build tools

Starting with esbuild 0.19.9, support for converting gradients with in hsl longer hue to equivalent normal gradients.

/* input */
.test-longer {
  background: linear-gradient(in hsl longer hue, red);
}

/* esbuild output */
.test-longer {
  background: linear-gradient(red, #ffbf00 12.5%, #ffef00, #dfff00 18.75%, #7fff00, #20ff00 31.25%, #00ff10, #00ff40 37.5%, #0ff, #0040ff 62.5%, #0010ff, #2000ff 68.75%, #7f00ff, #df00ff 81.25%, #ff00ef, #ff00bf 87.5%, red);
}

Opening esbuild's test page in Firefox 136+, we found that this compatibility is broken.

I can file an issue for esbuild at a later date and I believe it will be fixed soon.

Image

@csstools/postcss-gradients-interpolation-method may also be affected, I haven't tested it yet.

@yisibl
Copy link
Contributor

yisibl commented Feb 17, 2025

@jfkthame For cases smaller than 1px, Firefox's rendering is not quite the same.

Firefox renders as red, other browsers are gradient colors.

Image

data:text/html;charset=UTF-8,<!DOCTYPE html>
<style> div {
  width: 200px; height: 50px; border: 1px solid black;
  background:linear-gradient(90deg in hsl longer hue, 
  	transparent 0 var(--x), red var(--x) 100px, transparent 0 100%);
  --x: 99px;
}
</style>
<div></div>

This only seems to happen when the width is greater than 128px, and by zooming in we can see this more clearly.

data:text/html;charset=UTF-8,<!DOCTYPE html>
<style>
div {
  --w: 100px;
  width: var(--w); height: 5px; border: 1px solid black;
  background:linear-gradient(90deg in hsl longer hue, 
  	transparent 0 var(--x), red var(--x) calc(var(--w) / 2), transparent 0 100%);
  --x: calc(var(--w) / 2 - 1px);
  zoom: 10;
}
div:hover {
  --w: 130px;
}
</style>
<div></div>

@mysteryDate
Copy link

mysteryDate commented Feb 17, 2025

I would be very very worried about changing the appearance of sites that have come to depend on the current behavior, though maybe not many do. Is it possible to craft a query against HTTPArchive that looks for the gradient syntax that you are testing (i.e. gradient styles that hit the conditions of interest).

I would try to change the spec rather than browsers if this turns out to be something that has significant usage. Even though this does look to me like some kind of browser problem with the treatment of identical or near-identical stop colors.

For what it's worth, though the spec has existed for a long time, CSS hue interpolation methods have only been available in browsers for less than two years:

https://caniuse.com/mdn-css_types_gradient_linear-gradient_hue_interpolation_method

It was only shipped in Firefox last summer.

After rethinking, I also agree that it would be better for browsers to fix it, which prevents the specification from getting complicated and reduces the learning cost for users.

I agree with this approach.

aarongable pushed a commit to chromium/chromium that referenced this issue Mar 4, 2025
This CL added counts of Rainbow gradient pattern[1] that will not work
after this CL[2]

[1]w3c/csswg-drafts#11381 (comment)
[2]https://crrev.com/c/6253537

Bug: 387475844
Change-Id: I2db9c1e7e8dfd37fa7f0322ae3d9fc162a558128
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6320615
Reviewed-by: Rune Lillesveen <futhark@chromium.org>
Reviewed-by: Steinar H Gunderson <sesse@chromium.org>
Commit-Queue: Jason Leo <m.jason.liu@gmail.com>
Auto-Submit: Jason Leo <m.jason.liu@gmail.com>
Commit-Queue: Rune Lillesveen <futhark@chromium.org>
Commit-Queue: Steinar H Gunderson <sesse@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1427612}
tabatkins added a commit that referenced this issue Mar 11, 2025
@tabatkins
Copy link
Member

All right, at least for now I've updated the note about double-position creating a stripe, and added a note emphasizing that the spec means what it says about the color before the first stop and after the last stop. ^_^

Agenda+ to confirm that this should be closed No Change otherwise.

@svgeesus
Copy link
Contributor

That change looks correct to me.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

7 participants