Color Filters Can Turn Your Gray Skies Blue

Avatar of Amelia Bellamy-Royds
Amelia Bellamy-Royds on (Updated on )

The following is a guest post by Amelia Bellamy-Royds. I’ve always enjoyed the “duotone” effect in photos. In Photoshop, you can create them by converting an image into grayscale mode, then into duotone. So the lights are “mapped” to one color, and the darks another. Not only does it look cool, but images with less colors are smaller in file size and thus good for performance. When I saw Amelia playing around with this programatically through SVG on CodePen, I asked if she’d be up for teaching us through a guest post. Lucky for us, here it is!

Once upon a time, if you wanted artistic images in your web design, you created them in Photoshop. Or maybe GIMP, if you were edgy and open-source inclined. But either way, the end result was a single, static, image file that was uploaded to your server, downloaded to your user’s web browser, and displayed exactly as you created it. If you wanted to turn a graphical effect on and off in response to user interactions, then you exported two different image files, and you swapped them with JavaScript or CSS pseudo classes.

Graphical effects—first in SVG, now in CSS—are changing that. You can apply Photoshop-like filters or blended layers right in the browser. Which means you can use a single image file and present it in multiple ways as the user interacts with it. It also means you can have a lot of fun with a boring-old black and white photo.

Filter Effects Basics

The easiest-to-use graphical effects are the shorthand filter functions. You apply them with the CSS filter property The browser manipulates the appearance of the corresponding element before painting it to the page. Filter functions are supported prefix-free in Firefox since version 35 (stable release in January 2015). They have been supported with the -webkit- prefix in Safari, Chrome, and Blink-based Opera for a few years now.

(We’ll get into alternatives that work in Internet Explorer at the end of the post; for now, you’ll want to use one of those browsers to check out the demos!)

For a full description of the available filter functions (with examples!), check out the Almanac page on the filter property.

Some things to note:

  • You can apply a series of filters as a white-space separated list.
  • Filters create a stacking context.
  • A filter is applied to an entire element (including text, background images, and all its child content), after layering them together.
  • There are still a number of buggy edge cases in browsers, like what happens when there are fixed-position child elements or hidden overflow.

An easy and useful trick is to take a color image and convert it to black and white, using grayscale(100%) or saturate(0%). The following demo uses that to create monochrome profile pictures for a pair of smart cookies. The filter is removed on hover/focus to reveal the full color image. To compensate for the loss of color contrast and vibrancy, the overall brightness and contrast of the image is increased using the (conveniently named) brightness() and contrast() filter functions. It even adds a short transition to fade the colors in and out.

See the Pen CSS Filters Gray -> Color by Amelia Bellamy-Royds (@AmeliaBR) on CodePen.

This is all well and good. Take a color image, tell the browser to make it gray. Selectively reverse for user-delighting interaction.

But what if you could do the opposite? What if you could start with a grayscale image and tell the browser to give it color?

A grayscale JPEG image is usually between 5-25% smaller in file-size than the equivalent color photo (it depends on the photo and the compression ratio—JPEG compresses color channels much more than the brightness details). For a lossless image format such as PNG, the change in file size can be much greater. When performance matters, those extra kilobytes could count. Imagine if you had a page full of profile pictures. The user is only ever going to see some of them in color, so why use up their data plan sending the color photos? Filters also take CPU to process and memory to store the result. Wouldn’t it be better to only filter the hovered photo, and leave the rest as normal?

Finally, you have to think of the impact on browsers that don’t support CSS filters. Do you want all color or all grayscale as the fallback?

There is only one shorthand filter function that can add color to a grayscale image: sepia(), which gives whites and grays a yellowish tinge. On a color image, it applies an implicit conversion to a luminance-based grayscale first. It also increases the overall brightness and contrast, although not as much as the previous example did with separate filters. The demo below looks almost identical to the previous one, before you interact with it, but this time the images are actually saved as grayscale JPEGs. A sepia-toned effect is applied to add a little (false) color on hover.

See the Pen CSS Filters Gray -> Sepia by Amelia Bellamy-Royds (@AmeliaBR) on CodePen.

It’s a neat effect, but I can’t say I’m in love with the particular shades of yellow-beige that sepia tone implies. Can’t I pick my own colors?

I can. Just not with a shorthand filter function. These shorthand functions can all also be written longhand, as SVG filter elements. If you want more custom control, you just have to create the equivalent SVG filter and customize it yourself.

SVG filters are defined with a <filter> element inside an <svg>. For more on the overall syntax, check out Joni Trythall’s great post on SVG lighting filters from September 2014. To use the filter, you give it an id, and then use a URL reference in the filter property of another element:

.filter-this {
  filter: url(#myFilter);
}
.more-filters-please {
  filter: url("/assets/filters.svg#filter-1");
}

Within SVG, SVG filters are supported on most modern browsers, including Internet Explorer 10 and 11. Note the “within SVG”. SVG filters are not supported IE or Edge (yet) on HTML content. Users of IE9 and Opera Mini won’t see the effect. (There are also a few bugs and limitations in other browsers, but none of which affect the color filters discussed here.) For applying SVG filters to non-SVG content, Firefox and the latest versions of Chrome and Opera support it in the unprefixed filter property. Safari and older Blink support it in the prefixed -webkit-filter property.

The sepia() shorthand filter function is defined in SVG filters using the <feColorMatrix> operator. To create your own colorizing effect, therefore, you can create a color matrix filter and adjust the parameters.
Here’s what the SVG filter for 100% sepia looks like:

<filter id="sepia">
 <feColorMatrix type="matrix"
     values="0.393 0.769 0.189 0 0
         0.349 0.686 0.168 0 0
         0.272 0.534 0.131 0 0
         0   0   0   1 0"/>
</filter>

Easy-peasy, right? Just tweak a couple numbers here, a couple numbers there, and… realize you have absolutely no clue what you’re doing.

Time for a diversion into the wonderful world of matrix algebra.

The Math Part

The first thing you need to understand about <feColorMatrix> is that the list of numbers in the values make up a matrix. Specifically a five-column, four-row matrix as indicated above through the power of preserved whitespace. The output color for any pixel is created by multiplying that matrix against the input pixels.

(Side note: This only applies when type="matrix". The other type options are saturate and hue-rotate, which take a single number value and have shorthand CSS equivalents, and luminanceToAlpha, which doesn’t take any values.)

If high school algebra classes are lost in the mists of memory, here’s a refresher on matrices and matrix multiplication:

  • A matrix is basically a shorthand way of describing a set of algebraic equations. Instead of using letters like x and y to describe the variable amounts, you use the position within the rows and columns of the matrix.
  • A multi-part value—like an x,y point or an RGBa color—can be represented by a single-row or single-column matrix, otherwise known as a vector.
  • When you multiply a matrix and a vector, you are substituting in the values from the vector for variables in the matrix, and then adding up the result for each equation.

The color matrix filter is created by representing the RGBa input color as a vector where the values on each channel have been scaled to the range 0–1. That vector is multiplied by the matrix, and the result is then converted back to create the RGBa output color. Each vector and the matrix are given an extra row/column so that you can shift the colors by a fixed amount if you need to.

The matrix equation, using variables of the form Nra or Ca to represent the matrix parameters, looks something like this:

|R2|   | Nrr Ngr Nbr Nar Cr|   |R1|
|G2|   | Nrg Ngg Nbg Nag Cg|   |G1|
|B2| = | Nrb Ngb Nbb Nab Cb| * |B1|
|A2|   | Nra Nga Nba Naa Ca|   |A1|
| 1|   | 0   0   0   0   1 |   | 1|

Which can also be written as a series of five equations:

R2 = Nrr*R1 + Ngr*G1 + Nbr*B1 + Nar*A1 + Cr*1
G2 = Nrg*R1 + Ngg*G1 + Nbg*B1 + Nag*A1 + Cg*1
B2 = Nrb*R1 + Ngb*G1 + Nbb*B1 + Nab*A1 + Cb*1
A2 = Nra*R1 + Nga*G1 + Nba*B1 + Naa*A1 + Ca*1
1 =  0*R1 +  0*G1 +  0*B1 +  0*A1 + 1*1

The last row works out as 1=1, so you can safely ignore it. It is also ignored in the <feColorMatrix> attributes, which only specify the first four rows of the matrix (20 numbers total).

For the other rows, you are creating each of the rgba output values as the sum of the rgba input values multiplied by the corresponding matrix value, plus a constant. If you haven’t figured out my naming convention yet, Nra is the proportion of the original Red-channel that goes into the output Alpha channel, while Ca is the constant added to the Alpha channel.

A simple example of a color matrix is the one used for the luminanceToAlpha color matrix type. It is equivalent to the following matrix:

| 0      0       0      0 0 |
| 0      0       0      0 0 |
| 0      0       0      0 0 |
| 0.2126 0.7152  0.0722 0 0 |
| 0      0       0      0 1 |

Because the values in the first three rows are all zero, the output for the red, green, and blue channels is also zero (i.e., black). The output for the Alpha channel is created as a function of the three input color channels: mostly from the green, with a little bit from the red and a smidge from the blue. If the input color is pure white (i.e., the RGB values are all 1 after being scaled), then the alpha value is

A2 = 0.2126*1 + 0.7152*1 + 0.0722*1 = 1

In other words, completely opaque. If the input color is pure black (i.e., the rgb values are all 0), then the alpha value is

A2 = 0.2126*0 + 0.7152*0 + 0.0722*0 = 0

In other words, completely transparent. Any other color ends up with an alpha value somewhere in between. Brighter colors end up more opaque and darker colors end up more transparent. The specific numbers assigned for the RGB channels are intended to reflect the perceived differences in brightness between intense red, intense green, and intense blue. (The numbers are fixed in various standards, but the science isn’t actually perfect. The brightness you see is affected by the type of monitor (or printer) you are using as well as your own eyes’ sensitivity to each color.)

So, to go back to the sepia matrix:

0.393 0.769 0.189 0 0
0.349 0.686 0.168 0 0
0.272 0.534 0.131 0 0
0     0     0     1 0

The fifth row, as mentioned before, is excluded because it never changes. The matrix does two things: it flattens a colored image to grayscale (with luminance adjustments), and then it tints that color yellowish. The alpha channel does not change: the output A2 value is exactly 1 times the input A1 channel.

When the input color is black, the output is still black, because all the numbers will be multiplied by 0. However, when the input color is white, the output is as follows:

R2 = 0.393*1 + 0.769*1 + 0.189*1 = 1.351
G2 = 0.349*1 + 0.686*1 + 0.168*1 = 1.203
B2 = 0.272*1 + 0.534*1 + 0.131*1 = 0.937

The color is therefore rgb(135.1%, 120.3%, 93.7%). Now, wait, you’re saying “hey, you can’t have rgb values greater than 100%.” You can’t, but you can. They get clamped down to 100%, and so the result is that white parts of the input image get turned into a pale yellow. If you reduce the input values slightly, you won’t actually see a reduction in the red and green channels until the output drops below 100%, so you’ll still get yellow, but not as pale (since the blue channel will visibly drop off).

For a medium input, with a scaled value of 0.5 in each channel, the output color would be calculated as follows:

R2 = 0.393*0.5 + 0.769*0.5 + 0.189*0.5 = 0.6755
G2 = 0.349*0.5 + 0.686*0.5 + 0.168*0.5 = 0.6015
B2 = 0.272*0.5 + 0.534*0.5 + 0.131*0.5 = 0.4685

In other words, it would be an orangish-yellow-gray, approximately rgb(68%, 60%, 47%).

Lovely.

If you’re good with algebra and/or arithmetic, you might notice that the output values from the medium-gray input are exactly half of what they were for the white input. When the input picture is in shades of gray, you’re always going to have the same value for each of the red, green, and blue input channels. As a result, you could simplify the math a lot by writing it like this:

R2 = 1.351 * gray
G2 = 1.203 * gray
B2 = 0.937 * gray

To create a color matrix with the same result, you could set any one of the RGB columns in each row to those values, and leave the others as zero:

| 1.351 0  0  0 0 |
| 1.203 0  0  0 0 |
| 0.937 0  0  0 0 |
| 0     0  0  0 0 |

or

| 1.351  0  0     0 0 |
| 0   1.203 0     0 0 |
| 0   0     0.937 0 0 |
| 0   0     0     0 0 |

So long as the input image is grayscale, these will have the exact same result as the original sepia filter matrix. Which is good to know, because this blog post is all about colorizing grayscale images. (You may have forgotten after all that math.)

Before we get back to the demos, I should inject a cautionary note. You might be under the impression that in order to create a RGB input vector with a value of 0.5 in each channel, you should use rgb(50%, 50%, 50%) or #888 or gray. You would be right if you were using an <feColorMatrix> filter, but wrong if you were using the sepia() shorthand function. This is because the shorthand filters all use the sRGB color model to scale the input colors into the values used in the calculations.

SVG filters, in contrast, by default use a direct mathematical conversion between the input RGB values and the values used in the matrix mathematics. This can be controlled with the color-interpolation-filters property, which can be set on the filter as either an attribute or an (inheritable) CSS property. The two values for the property are linearRGB and sRGB.

The rest of the examples are going to use sRGB. The sRGB mode most effectively preserves the perceived brightness differences between parts of the input image. But it has a significant impact on the result, so if you are using the same numbers and getting very different effects, that might be it. It also means that the colors can’t easily be calculated by hand. It is particularly tricky to define filters that exactly create a particular output color for medium grays. If you’re playing around with filters in the browser, that’s usually not a problem. However, if you’re trying to match particular colors in the rest of your web design, you may want to test out color-interpolation-filters: linearRGB;.

Monochrome Colorizing

The sepia filter creates a monochrome effect. The black-to-white grayscale values in the input are mapped to black-to-lightyellow instead. It’s not a perfect monochrome effect, because of the clamping effect on the over-saturated red and green channels, but it is something like it.

To create your own monochrome colorizing filter, you change the values in the simplified version of the sepia matrix. To create a black-to-peachy pink—approximately rgb(100%, 80%, 65%)— filter, you would use the following

<filter id="monochrome" 
    color-interpolation-filters="sRGB"
    x="0" y="0" height="100%" width="100%">
  <feColorMatrix type="matrix"
   values="1.00 0 0 0 0 
           0.80 0 0 0 0 
           0.65 0 0 0 0 
           0    0 0 1 0" />
</filter>

Here’s what it looks like. I’ve commented-out the bits that add the hover effect, so you can just see the end result.

See the Pen SVG Filters Gray -> Monochrome Color by Amelia Bellamy-Royds (@AmeliaBR) on CodePen.

If you’re familiar with monochrome printed images, the effect might actually be the opposite of what you consider to be a monochrome image. In printing, you usually control the color of the dark parts of an image. A printer monochrome image would therefore have a dark color-to-white transition. To create that effect with an RGB color monitor, we need to go back to our basic color matrix equations:

R2 = Nrr*R1 + Ngr*G1 + Nbr*B1 + Nar*A1 + Cr*1
G2 = Nrg*R1 + Ngg*G1 + Nbg*B1 + Nag*A1 + Cg*1
B2 = Nrb*R1 + Ngb*G1 + Nbb*B1 + Nab*A1 + Cb*1
A2 = Nra*R1 + Nga*G1 + Nba*B1 + Naa*A1 + Ca*1
1 =  0*R1 +  0*G1 +  0*B1 +  0*A1 + 1*1

When the input color is black, all of the Nij values disappear (multiplied by the 0 values in the RGB input channels). The only thing you can control are the constants in the final column. So if you want the dark parts of the image to be drawn in deep blue—rgb(5%, 15%, 50%), say—you need a matrix that looks something like the following:

? ? ? 0 0.05
? ? ? 0 0.15
? ? ? 0 0.50
0 0 0 1 0

The numbers in the question mark positions don’t have any effect when the input is black. They control what happens for the rest of the image. If they were all zero, every input pixel would have the same constant output, and the entire image would end up deep blue. There are much easier ways to create a solid deep blue region in CSS, so that’s probably not what you want.

As before, we can simplify things by remembering that our input image is grayscale and the RGB channels will always be equal. We only need to change one of the columns:

? 0 0 0 0.05
? 0 0 0 0.15
? 0 0 0 0.50
0 0 0 1 0

We want the output color to be white (all ones) when the input is white, so you might think you could replace each question mark with a 1. However, you’ve still got those constants added to every pixel. Once you add them in, you’ll have an over-exposed image with most of the lighter details washed out.

Instead, you need to calculate the amount you need to add to each channel to create a white final value for a white input value. In other words, the question marks are replaced by 1 minus the constant color that being used for black:

0.95 0 0 0 0.05
0.85 0 0 0 0.15
0.50 0 0 0 0.50
0    0 0 1 0

With that matrix, the result looks as follows:

See the Pen SVG Filters Gray -> Invert Monochrome Color by Amelia Bellamy-Royds (@AmeliaBR) on CodePen.

DuoTone Colorizing

So far, we’ve used color matrix filters to create both a black-to-color monochrome and a color-to-white monochrome. Why not color-to-color? Of course! That’s technically no longer a monochrome image, though: in printing, the effect is known as duotone.

The approach is the same as for the color-to-white matrix. You set the constants (the last column) to the value you want for the black parts of the input image, and then set one of the other columns to the difference between that and the color you want the white parts to display as.

So, to create a deep blue-to-peachy pink duotone, you could use the following matrix:

0.95 0 0 0 0.05
0.65 0 0 0 0.15
0.15 0 0 0 0.50
0    0 0 1 0

The white points will end up as rgb(95%+5%, 65%+15%, 15%+50%). Which is the same peachy-pink as before. However, all the intermediary grays will be somewhere in between that color and the deep blue.

The demo does just that:

See the Pen SVG Filters Gray -> Duotone Color by Amelia Bellamy-Royds (@AmeliaBR) on CodePen.

Of course, the color you chose for the white and black points does not have to be an achievable color. You are allowed to “over-expose” your colors in the same way as the sepia-toned filter does. You can also “under-expose” certain color channels on the black points by setting them to negative values. For significant switches in color, the difference between the black point and the white point might also be negative. (You could even use this approach to invert the colors to create a photo-negative effect, although there is a CSS shorthand function for that purpose.)

The next demo uses more saturated colors to show off the use of negative color shifts and color values that fall outside the 0–1 range.

See the Pen Psychedelic SVG Filters Gray -> Duotone Color by Amelia Bellamy-Royds (@AmeliaBR) on CodePen.

While looking at that demo, you might be surprised to note that the box-shadow on the image also turns purple. Filters are applied after all other CSS effects, except for clipping and masking. If you want to create a shadow that doesn’t change color with the filter, you’ll have to use a filter to create it. You could do this within the SVG filter, or you can do it with the shorthand drop-shadow() filter function, which takes the same parameters as the text-shadow() property. It’s not quite the same as a box shadow (you can’t use a “spread” parameter, and it is affected by any transparent regions in the content), but in this case it’s darn close.

See the Pen v2: Psychedelic SVG Filters Gray -> Duotone Color by Amelia Bellamy-Royds (@AmeliaBR) on CodePen.

If you’ve made it to this point, you’re probably a little bored of my grinning face(s), so let’s try a different example. I forked the next pen from an example Chris Coyier created for a post on layering text over top of images. To keep the text readable, you need to reduce a lot of the contrast and intensity of the image. The technique Chris uses (and which is very useful) is to layer a semi-transparent single-color gradient over top of the colored image.

The net result for many images is something that doesn’t look that much different from my colorized grayscale images. In the demo, the first version of each slide has a colored background image, while the second version is a duotone-colored grayscale.

See the Pen feColorMatrix – Tinted grayscale hero images by Amelia Bellamy-Royds (@AmeliaBR) on CodePen.

The duotone image is somewhat flatter, with lower contrast than the shaded color image. That is not necessarily a bad thing, given the desire to keep the text legible. But it might not be what you want.

The main limitation to the duo-tone approach I’ve been using so far is that, in order to create blue mid-tones, I needed to use dark blue values for my black point and light blue values for my midpoint.

In order to break free of this limitation, we have to break free of linear algebra. We need an exponential improvement on our colorizing abilities.

Gamma-Corrected Colorizing

By exponential improvement, I mean mathematical exponents. (What? You thought we were done with the math? Not yet!) In particular, I mean a gamma-correction factor.

A gamma correction is a common manipulation in digital imagery, to create a non-linear change in brightness or color intensity. Applied equally to all color channels, it increases or decreases the brightness of the mid-tones without over- or under-exposing the lights and darks. Applied to each color individually, it can shift the overall color balance of an image, without shifting blacks and whites.

The <feColorMatrix> duotone effect can be thought of as picking two colors in the sRGB color cube, and drawing a straight line between them. All the shades of gray are mapped to a value along that line.

If you applied color-specific gamma factors to that line, you could create a curved line through the three-dimensional color space. It can start at black and end at white, but still shift through reds, blues, or greens instead of pure grays.

You can’t apply gamma factors with matrix multiplication. However, you can with the <feComponentTransfer> filter. It supports a variety of different functions on color channels, but only one channel at a time. Each channel has its own element to describe the conversion function that will be applied: <feFuncR>, <feFuncG>, <feFuncB>, and <feFuncA>. Each function element has a type attribute to describe the math that will be used, and then various other attributes give the parameters for each type.

(Side note: We could actually have implemented many of the above filters with <feComponentTransfer>, although it would have increased the markup slightly. You can use a linear type for each color function, which multiplies the input value by a slope parameter and adds a fixed intercept to it. The intercepts would be the black-point values and the slopes would be the amount of change required to shift to the white point. You can’t make one color’s output conditional on another color’s input, but with grayscale inputs that isn’t relevant.)

A basic gamma-adjusted color filter looks like this:

<filter id="gamma-red" 
    color-interpolation-filters="sRGB"
    x="0" y="0" height="100%" width="100%">
  <feComponentTransfer>
    <feFuncR type="gamma" exponent="0.5" />
    <feFuncG type="gamma" exponent="1.1" />
    <feFuncB type="gamma" exponent="1.4" />
  </feComponentTransfer>
</filter>

The resulting equations are as follows:

R2 = R1^0.5
G2 = G1^1.1
B2 = B1^1.4

When an input color channel is 0, the output is also 0 (0 raised to any power is still 0). When an input channel is 1, the output is also 1 (1 raised to any power is still 1). For all the in-between values, exponents less than 1 increase that color in the output, and exponents greater than 1 decrease the color in the output. This is the inverse of how gamma factors are defined in many software programs, which use an exponent of 1/gamma.

As shown in the following demo, the gamma-red filter given above creates an image where blacks are still black, whites are still white, but all the grays have been mapped to shades of burgundy red:

See the Pen SVG Filters Gray -> Non-linear Color by Amelia Bellamy-Royds (@AmeliaBR) on CodePen.

The gamma-type component transfer function is more powerful than that, however. In addition to specifying an exponent you can specify two other attributes: amplitude and offset. The amplitude is a factor multiplied with the result of raising the input value to the exponent factor. The offset is a constant value added afterwards. In other words, you final equations look more like this:

R2 = Nr * R1^Er + Cr
G2 = Ng * G1^Eg + Cg
B2 = Nb * B1^Eb + Cb

The constant offsets still set the output color for input black pixels. The sum of those constants plus the amplitude factor sets the output color for input white pixels. The exponents define how your color line will curve in between those two points. You could try to figure it out the exact values from the equations, but it is probably easier to open it up in a live editor and fiddle with the numbers until it looks right to you.

Which is how I came up with this version of the skyscraper demo:

See the Pen feComponentTransfer – Tinted grayscale hero images by Amelia Bellamy-Royds (@AmeliaBR) on CodePen.

Can you tell which one is the real color image? Could you tell if they weren’t side by side?
Not all images will be this easy to reproduce, of course. You can never get a full multi-colored image from a grayscale one. The skyscrapers work because the original image had very limited color variation. But, that limited color variation was the main reason that the image was so suitable as a backdrop. If that’s the effect you’re trying to go for, you can definitely create it from a grayscale photo.

Pity the Poor Internet Explorer Users

As I mentioned at the top of the post, support for SVG filters on HTML content goes back a couple years on Firefox and (with -webkit- prefixes) Chrome, Opera, and Safari. It is expected to also be supported in Microsoft Edge. But in the meantime, there are still a large chunk of web users out there without beautiful filter goodness for their web page experience.

Support for SVG filters within SVG content, however, is much better (IE10+). And SVG content can include embedded JPEG images. You can therefore quickly improve you browser support by moving the images you want to filter into inline SVG tags. It requires a little extra markup, but the end result looks the exact same.

See the Pen IE10+ Tinted grayscale hero images by Amelia Bellamy-Royds (@AmeliaBR) on CodePen.

Nonetheless, this still isn’t universal support. If your boss or client insists that the website must look the same on every browser including Internet Explorer 8, you’ll be doing some manual or server-side image processing to create colored versions of all the effects, and some scripting (client or server side) to swap them in.

For more pragmatic-minded fallbacks, your main concern is whether the interface is usable and text is legible. For the grayscale-to-color hover effect, that can be as simple as making sure there are other interface cues that something is interactive. For text overlaid on an image, it is a little trickier. Without the filter, the grayscale images used in the skyscraper demo are too bright and high-contrast to be able to read the text. One solution would be to darken the images at the time they are converted to gray, so that the filter only handles the colorizing. Other approaches would require browser-support testing followed by changing other style properties.

Unfortunately, client-side testing for filter support is a little tricky. You can’t just test for whether the property is supported, since that does not tell you if it will have any effect on HTML elements. The CSS @supports rule fails for the same reason. You can test whether the shorthand functions are supported (by setting the filter style of a dummy element to the function string and seeing if it is still there when you read back the computed style), or you can test whether filters are supported in SVG (by confirming that you can create an SVG element, and that it has the element.style.filter property), but you can’t directly test whether filter: url(#blur) will have any effect when declared on a <div>.

Final Thoughts

Filters are only one of the new graphical effects that are changing what is possible with images on the web. Blending modes and masking will have an equal impact. Both the monochrome and duotone color effects can also be achieved with blending and with masking. As with most things in CSS (and the web in general), there are many ways to achieve a given effect.

When deciding which approach to use, consider your support and fallback options, as well as the impact on your code maintainability. Blended backgrounds are convenient in that they can be created without any extra markup. But an SVG-filtered image within an inline <svg> has the best support.

One limitation of using SVG filters instead of the CSS shorthand filter functions is that url() references cannot be animated or transitioned. You can animate the attributes of the filter itself using SMIL animation elements or JavaScript. However, that affects all uses of the filter, not just a single hovered element. Alternatively, you can take an old-school approach and use two copies of the image—one filtered and one not—and change the opacity of the top layer to slowly reveal the bottom one. Unlike the old-school technique, your users would still only need to download one file.