CSS gradients are amazing, wonderful things for what they can do. But there’s still a lot they legitimately can’t do. There are tricks people have used to try making conical/angular gradients, but recently I came across a need to use a bilinear gradient.
For those who haven’t run into this before, a bilinear gradient is what you would use on a square where each corner is a different color. Essentially on one side you’d have a gradient that went from color A to color B, and on the opposite side would be one from C to D, and as you moved between those sides you’d want one gradient to transition to the other. When the shape is not a square the math gets more complicated, but this is the gist.
The reason this came up for me was that I was working on a color picker using only CSS gradients. (If I’d used SVG it wouldn’t have made much difference, though; SVG has some more options that would fit what I needed, but they’re not widely supported yet.) Specifically, this picker would use a 2D selection area and a slider; the slider would represent the current mode of the picker, for instance hue (the default), while the 2D area would be two other parameters of the color, e.g. saturation and luminance. (In hue mode, though, I cheated and went with HSV instead of HSL because it looks better.) I had modes for hue, saturation, and luminance, but wanted to do red, green, and blue as well. This meant the 2D area had to transition from, say, no red on the left to full red on the right, and no blue on the bottom to full blue on the top. That’s a bilinear gradient because each corner has a diferent color and there’s no simple way to add them together.
After trying and failing to get anything right with diagonal gradients and varying the alpha of the top layer, I finally had a moment of clarity: This particular bilinear gradient can be done if one gradient is vertical, one is horizontal, the one on top has 50% opacity, and then the color level is doubled. First problem: CSS filters aren’t widely supported yet, so not so much with the color doubling.
However, this turned out to be only a minor setback. I realized that if I split my square into quadrants, I could fine-tune the gradients to get exactly the effect I wanted without needing to double the color values.
Here’s the breakdown, using green editing mode as an example. So green stays constant, and for the purposes of this example I won’t mention it again. Red increases on the horizontal axis, blue on the vertical. The left two quadrants each have a vertical gradient from black to blue, and the right two quadrants each have one from red to magenta. The horizontal gradients that go over these will be at 50% opacity. The bottom two quadrants have a horizontal gradient going from black to red, and the top two from blue to magenta.
Since gradients can repeat, this really only calls for four divs. One is the container, and I arbitrarily chose for it to be the left half; it’s full width and full height, but its right half will be covered up by the next div, which is in its contents. The next div is only half width and is positioned on the right-hand side. Each of these gets the appropriate vertical gradient. Then two more divs are placed in the container, each with full width but 50% height and opacity, one on top and one on the bottom, and these are for the horizontal gradients. This is roughly the code:
<div style="width:250px; height:250px; position: relative; border: 1px solid black;">
<!-- left half (contains others half-divs) -->
<div style="width:100%; height:100%; position: absolute; left: 0; top: 0; background: linear-gradient(to top, #000, #00f 50%, #000 50%, #00f);">
<!-- right half -->
<div style="width:50%; height:100%; position: absolute; left: 50%; top: 0; background: linear-gradient(to top, #f00, #f0f 50%, #f00 50%, #f0f);"></div>
<!-- bottom half -->
<div style="width:100%; height:50%; position: absolute; left: 0; top: 50%; background: linear-gradient(to right, #000, #f00 50%, #000 50%, #f00); opacity: 0.5;"></div>
<!-- top half -->
<div style="width:100%; height:50%; position: absolute; left: 0; top: 0; background: linear-gradient(to right, #00f, #f0f 50%, #00f 50%, #f0f); opacity: 0.5;"></div>
Unfortunately I can’t display the result here with raw markup; the only way to do that is to use an image. CSS gurus will note that the outer div with the border has a specific size and also has position: relative so that it is an offset parent of the first element within. This method will work regardless of what the green value is, as long as it’s consistent between all four divs.
The reason this trick works is that ultimately we want the red and blue components to be additive. The 50% blend would be perfect for that except that it darkens the result. But if we go by quadrants, we can make sure that the corner corresponding to that quadrant is a common color in both gradients. E.g., in the upper right we go from blue to magenta, and red to magenta. Magenta averaged with itself is the same; and for all the others, they end up being mathematically exactly what we want for where they are on the full square.
Sadly, this technique is not applicable to generic bilinear gradients. That is, you can’t just take any old four colors and expect this to work. This method only works because the red value increases by the same amount regardless of where you are vertically, and blue increases by the same amount regardless of where you are horizontally. If the red and magenta corners got swapped, for instance, it wouldn’t work at all. (Funnily enough, though, it’d still be possible to simulate that gradient a different way.) Or if in place of magenta you had yellow, that’d fail.
Given that I was able to puzzle this out with some difficulty, though, I can’t help but wonder if there is a general solution that simply eludes me: some combination of splitting up the shape into components where a more simple layering can take over. I believe there might be a general solution if conical/angular gradients were available, but they’re not. Radial gradients don’t seem as if they’ll ever hold any promise for this sort of thing.