I came across an unfortunate and confusing problem recently. I was after a quit simple and common effect: an element with a semiopaque background pattern that faded in from the top.
My first instinct to solve this problem was the most obvious: let's give an element the background image (in this case, stars) and then apply a gradient on top of it. The gradient would be transparent at the bottom and the exact same color of the page's background at the bottom.
Here's that first attempt:
Depending on your device hardware and display, the problem might be very apparent to you.
Here's a closer look, with increased brightness and contrast to get an idea of what's happening:
It's color banding!
Why!? I gave the element a background image, and then
Color banding is the term for when you can see the individual colors in a gradient instead of a nice and smooth fade.
The problem is that any given display (and any given underlying color space) can only produce a finite set of colors. When your gradient covers more pixels than it has unique colors to fill them with, you start to see color bandingβbecoming more pronounced as the gradient gets larger.
A the start of the article, I explained that my technique for this effect was to overlay the background image with a gradient of the same exact color as the background!
How can there be color banding between two identical colors?
The alpha channel itself is being dithered and posterized. The browser is performing this step when it's rendering the gradient with no consideration of what the colors are below it. The eventual composition of the gradient over the matching background color is moot when the gradient itself is being rendered with a limited color space.
The solution is to use a mask instead of a gradient overlay.
Since the mask will affect the opacity of both the background image and of the element's text and all of its children, we should apply it to a pseudo-element.
.stars::before {
content: '';
position: absolute;
inset: 0;
background-image: url('/images/assets/stars.png');
mask: linear-gradient(to bottom, transparent, black);
}
Notice the colors!
In the case of the mask, we use a gradient that fades from transparent to blackβnot the page's background color as before!
The CSS mask property lets us hide parts of an element using an image. In our case, the image is a gradient created with the linear-gradient
function. The parts of the mask that are transparent or semitransparent become the same in the resultant image.
Let's give it a try:
Perfect! The banding problem is solved.