Detect Inverted Color with CSS and JavaScript
There was something that bugged me after reading David's article on the invert
filter last week. It was this sentence:
The values reported back by
window.getComputedStyle(el)
will be the original CSS values, however, so there's no way of getting the true inverted values of given properties.
But if I can read the original value for background-color
or color
and I can read the value for the filter
, then I can compute the inverted value, which means there is a way after all.
Well, let's see how we do that.
Full inversion for a solid background
First of all we need to understand how the invert
filter works and we'll start with the particular case of full inversion - invert(100%)
. For an arbitrary original value rgb(red, green, blue)
, where red
, green
and blue
can take values between 0
and 255
applying an invert(100%)
filter is going to produce rgb(255 - red, 255 - green, 255 - blue)
.
This is pretty straightforward and easy to code, especially since the values obtained via window.getComputedStyle(el)
are rgb()
or rgba()
values in all browsers supporting CSS filters, depending on whether the initial value has an alpha channel that is less than 1
or not.
Note: CSS filters with filter function values (not just url()
values) are supported by Chrome 18+, Safari 6.1+, Opera 15+ and Firefox 34+. Chrome, Safari and Opera need the -webkit-
prefix, while Firefox doesn't need and never needed a prefix, but needs layout.css.filters.enabled
set to true
in about:config
. IE does not support CSS filters yet, though they are listed as "Under Consideration". If implemented, it's probably not going to need a prefix either. So don't use -moz-
or -ms-
prefixes for filters!
So if we have an element for which we set a background-color
and then we apply an invert(100%)
filter on it:
.box { background-color: darkorange; -webkit-filter: invert(100%); filter: invert(100%); }
Then we can read its styles and compute the inverted value from the original:
var box = document.querySelector('.box'), styles = window.getComputedStyle(box), original = styles.backgroundColor, channels = original.match(/\d+/g), inverted_channels = channels.map(function(ch) { return 255 - ch; }), inverted = 'rgb(' + inverted_channels.join(', ') + ')';
It can be seen working in this pen, where the first box has the original background-color
and no filter applied, the second one the same background-color
and an invert(100%)
filter, while the third one has a background that's the inverted value (as computed by the JavaScript code above) of the original background-color
, assuming an invert(100%)
filter.
See the Pen Getting the inverted value for `background-color` via JS #1 by Ana Tudor (@thebabydino) on CodePen.
What about semitransparent backgrounds?
But this is only for rgb()
and if our background-color
isn't fully opaque, then the value we read with JavaScript is going to be an rgba()
value.
For example, the backgroundColor
style value read via JavaScript is going to be "rgb(255, 140, 0)"
if, in the CSS, we set background-color
to any of the following:
darkorange
#ff8c00
rgb(255, 140, 0)
rgb(100%, 54.9%, 0%)
rgba(255, 140, 0, 1)
rgba(100%, 54.9%, 0%, 1)
hsl(33, 100%, 50%)
hsla(33, 100%, 50%, 1)
But it's going to be "rgba(255, 140, 0, .65)"
if we set background-color
to one of the following:
rgba(255, 140, 0, .65)
rgba(100%, 54.9%, 0%, .65)
hsla(33, 100%, 50%, .65)
Since the invert
filter leaves the alpha
channel unchanged and only modifies the red
, green
and blue
channels, this means that our JavaScript code becomes:
var box = document.querySelector('.box'), styles = window.getComputedStyle(box), original = styles.backgroundColor.split('('), channels = original[1].match(/(0\.\d+)|\d+/g), alpha = (channels.length > 3)?(1*channels.splice(3, 1)[0]):1, inverted_channels = channels.map(function(ch) { return 255 - ch; }), inverted; if(alpha !== 1) { inverted_channels.splice(3, 0, alpha); } inverted = original[0] + '(' + inverted_channels.join(', ') + ')';
It can be seen working in this pen.
See the Pen Getting the inverted value for `background-color` via JS #2 by Ana Tudor (@thebabydino) on CodePen.
Note that this assumes that the value of the alpha channel of the background we set in the CSS is always greater than 0
. If the value of the alpha channel is 0
, then the value returned by styles.backgroundColor
is going to be "transparent"
and inversion really has no point anymore.
Alright, but this was all for a filter
value of invert(100%)
. What happens if we want to have a value that's less than 100%
?
General case
Well, for a value less than 100%
, let's say 65%
, the [0, 255]
range for each of the red
, green
and blue
channels is first squished around its central value. This means that the upper limit of the range goes from 255
to 65%
of 255
and its lower limit goes from 0
to 100% - 65% = 35%
of 255
. Then we compute the squished equivalent of our original background-color
which simply means taking each channel value, scaling it to the new squished range and then adding the lower limit. And finally, the last step is getting the symmetrical value with respect to the central one for each of the red
, green
and blue
channels of the squished equivalent of our original background-color
.
The following demo visually shows all this happening, taking the green
channel as an example.
See the Pen Squishing a channel range by Ana Tudor (@thebabydino) on CodePen.
The original range goes from 0
to 255
and the original value for the green
channel is taken to be 165
. Any value less than 100%
for the argument of the invert
filter function squishes the range. A value of 50%
means that the range is squished to nothing, as the upper limit equals the lower one. This also means that the final inverted rgb()
value is always rgb(128, 128, 128)
, no matter what the initial background-color
may be.
The squished equivalent of the original value for the green
channel is the one having an s
in front on its label. The inverted value of this squished equivalent with respect to the middle of the range has an i
in front on its label. And we can see that this is the value of the green
channel for the solid background of the second box at the bottom after applying an invert
filter on it.
This means that our JavaScript code now becomes:
var box = document.querySelector('.box'), styles = window.getComputedStyle(box), invert_perc = (styles.webkitFilter || styles.filter).match(/\d+/), upper = invert_perc/100*255, lower = 255 - upper, original = styles.backgroundColor.split('('), channels = original[1].match(/(0\.\d+)|\d+/g), alpha = (channels.length > 3)?(1*channels.splice(3, 1)[0]):1, inverted_channels = channels.map(function(ch) { return 255 - Math.round(lower + ch*(upper - lower)/255); }), inverted; if(alpha !== 1) { inverted_channels.splice(3, 0, alpha); } inverted = original[0] + '(' + inverted_channels.join(', ') + ')';
However, there is a problem: this assumes that the computed style value for the filter property is something like "invert(65%)"
. But most of the time, this is not the case. If we set the filter value to invert(65%)
in the CSS, then the value we get this way via JavaScript is "invert(0.65)"
in WebKit browsers and "invert(65%)"
in Firefox. If we set the filter value to filter(.65)
in the CSS, then the value we get via JavaScript is "invert(0.65)"
. Firefox returns it in the same format as the one we have set it in the CSS, while WebKit browsers always return it as a value between 0
and 1
. So we need to handle this as well:
var box = document.querySelector('.box'), styles = window.getComputedStyle(box), filter = (styles.webkitFilter || styles.filter), invert_arg = filter.match(/(0\.\d+)|\d+/)[0], upper = ((filter.indexOf('%') > -1)?(invert_arg/100):invert_arg)*255, lower = 255 - upper, original = styles.backgroundColor.split('('), channels = original[1].match(/(0\.\d+)|\d+/g), alpha = (channels.length > 3)?(1*channels.splice(3, 1)[0]):1, inverted_channels = channels.map(function(ch) { return 255 - Math.round(lower + ch*(upper - lower)/255); }), inverted; if(alpha !== 1) { inverted_channels.splice(3, 0, alpha); } inverted = original[0] + '(' + inverted_channels.join(', ') + ')';
As it can be seen in this pen, it now works properly for both formats, provided that the alpha
channel isn't 0
.
See the Pen Getting the inverted value for `background-color` via JS #3 by Ana Tudor (@thebabydino) on CodePen.
Final words
This still only deals with the very simple case where the value of the filter
property is just one invert
function. It won't work when we have chained filter functions, for example, something like filter: hue-rotate(90deg) invert(73%)
.
About Ana Tudor
Loves maths, especially geometry. Enjoys playing with code. Passionate about experimenting and learning new things. Fascinated by astrophysics and science in general. Huge fan of technological advance and its applications in all fields. Shows an interest in motor sports, drawing, classic cartoons, rock music, cuddling toys and animals with sharp claws and big teeth. Dreams about owning a real tiger.
It is important to note that the original article was talking about the invert filter inverting the colors of all content, not just the background color and the text color. If you notice the examples on that page you will see that the image’s colors are inverted. You will also notice that if you apply the filter to videos and other content that even the colors in the content are affected.
Is that a challenge? :P I believe it’s possible to invert images and videos frame by frame with JS using canvas (well, with the same domain restriction because I need to get the image data).
A small (but great!) update –
– Firefox 35 will have CSS filters enabled by default, without changing anything in about:config.
https://developer.mozilla.org/en-US/Firefox/Releases/35#CSS
– Internet Explorer now lists CSS filters as “In Development”! We might just see them working in Internet Explorer 12.
https://status.modern.ie/filters?term=filters
Yup, seen that, can’t wait! Firefox 35 should become stable in early January as far as I know. No clue about when they might actually land in IE at this point.