A PostCSS plugin to fix gradient transparency for older versions of the Safari browser (anything earlier than version 15.4).
Finds all instances of the transparent
keyword being used in CSS gradients and tries to replace them with specific colour values.
Back when the CSS gradients specification was first written, it defined colour transitions as simple interpolations in the RGB colour space. A lot of web developers started being caught out by gradients that faded to full transparency, and noticed dark greys in their gradients.
The reason for the darkness is that the CSS keyword transparent
is actually an alias for rgba(0, 0, 0, 0)
— that is, fully transparent black. A simple definition like linear-gradient(red, transparent)
would not only fade the colour from fully opaque to fully transparent, but it would also fade from red to black at the same time.
While this was correct from a technical view, it was unintuitive behaviour to web developers. Eventually the spec was changed to codify the use of a special graphics technique called pre-multiplied alpha. While the specifics of this technique are not important here, the results are. Effectively it means that gradients fading to/from a fully transparent colour now eliminate the “fade to black” part, and look much more like developers expect them to.
Of course, there was a catch — not all the browsers implemented the updated version of the spec at the same time. If you wrote a gradient with a transparent
value, it would look the way you intended in some browsers but not in others.
When this plugin was first written in 2016, some browsers had started to support the new spec. Very soon afterwards, Safari was left as the only browser that supported CSS gradients but did not support the updated spec. This applied to both desktop and iOS Safari.
In March 2022, Safari 15.4 was released with full support for pre-multiplied alpha transparency, completing the last piece of the support puzzle. But not all devices can upgrade to 15.4, so a compatibility solution is still required.
The solution for those older browser versions is to not use the transparent
keyword at all, but instead use specific rgba()
or hsla()
values that have full transparency but keep the colour the same:
/* Original */
.thingy {
background-image: linear-gradient(green, transparent);
}
/* Compatible version */
.thingy {
background-image: linear-gradient(green, rgba(0, 128, 0, 0));
}
If you have colours either side of a transparent
keyword, you need to create two transparent colour stops at the same position in order to keep the colour transitions the same:
/* Original */
.thingy {
background-image: linear-gradient(green, transparent 50%, blue);
}
/* Compatible version */
.thingy {
background-image: linear-gradient(green, rgba(0, 128, 0, 0) 50%,
rgba(0, 0, 255, 0) 50%, blue);
}
If you’re manually editing your gradients to do this, it can become easy to make mistakes — especially if you have to convert from #rgb
hex values in the process.
That’s where this plugin comes in. You can continue to author your gradients with transparent
values, and the plugin will transform them into the more compatible version for you.
It will only transform transparent
values that are found in gradients. Using background-color: transparent
, for example, will be left unchanged.
Input:
.simple {
background-image: linear-gradient(transparent, red);
}
.keep-stop-positions {
background-image: linear-gradient(transparent 40%, #f00 60%);
}
.complex {
background: transparent radial-gradient(farthest-side at 30px 2em, red, transparent),
linear-gradient(hsl(230, 45%, 86%), transparent 3em, peachpuff);
}
Output:
.simple {
background-image: linear-gradient(rgba(255, 0, 0, 0), red);
}
.keep-stop-positions {
background-image: linear-gradient(rgba(255, 0, 0, 0) 40%, #f00 60%);
}
.complex {
background: transparent radial-gradient(farthest-side at 30px 2em, red, rgba(255, 0, 0, 0)),
linear-gradient(hsl(230, 45%, 86%), hsla(230, 45%, 86%, 0) 3em, rgba(255, 218, 185, 0) 3em, peachpuff);
}
Install the plugin via npm: npm install postcss-gradient-transparency-fix
Then include it in your project in the same way as other PostCSS plugins. For example:
postcss([ require('postcss-gradient-transparency-fix') ])
See the PostCSS docs for examples for your environment.
Only values of the keyword transparent
will be altered. Any other transparent colours, including rgba(0, 0, 0, 0)
, will be left unchanged as they could be specifically intended as those values.
For best results, define explicit stop positions for the transparent
colour stops. The plugin will try to guess missing stop positions where possible, but due to the nature of gradient calculations, some position values can only be calculated by the browser at the time of rendering. Do you really understand CSS linear-gradients gives many details of how browsers calculate stop positions.
Some examples of what is supported:
/* No positions at all, assumes 50% */
(red, transparent, green) -> (red, transparent 50%, green)
/* One stop has a percentage value, context can be calculated */
(red 50%, transparent, green) -> (red 50%, transparent 75%, green)
/* Surrounding stops have positions with the same unit type */
(red 30px, transparent, green 80px) -> (red 30px, transparent 55px, green 80px)
Examples of positions that cannot be calculated at all:
/* One stop has a non-percentage value, context depends on final rendered size */
(red 100px, transparent, green)
/* Surrounding stops have different unit types */
(red 100px, transparent, green 80%)
/* Surrounding stops use calc() */
(red 1em, transparent, green calc(100% - 1em))