Illustration by

Finessing `feColorMatrix`

Have you seen Spotify’s end-of-year campaign? They’ve created a compelling visual aesthetic through image-color manipulation.

Article Continues Below

Screenshot of Spotify’s end-of-year campaign

Image manipulation is a powerful mechanism for making a project stand out from the crowd, or just adding a little sparkle—and web filters offer a dynamic and cascadable way of doing it in the browser.

CSS vs. SVG#section1

Earlier this year, I launched CSSgram, a pure CSS library that uses filters and blend modes to recreate Instagram filters.


Image grid from Una Kravets’ CSSGram showing a variety of filters and blend modes that recreate Instagram filters

Now, this could be done with tinkering and blend modes—but one key feature CSS filters lack is the ability to do per-channel manipulation. This is a huge downside. While CSS filters are convenient, they are merely shortcuts derived from SVG and therefore provide no control over RGBA channels. SVG (particularly the feColorMatrix map) gives us much more power and lets us take CSS filters to the next level, granting significantly more control over image manipulation and special effects.

SVG filters#section2

In the SVG world, filter effects are prefixed with fe-. (Get it? For “filter effect.”) They can produce a wide variety of color effects, ranging from blur to generated 3-D textures. The term fe– is a bit loose, though; see the end of this article for a summary of each of the SVG filter effects’ methods.

SVG filters are currently supported in the following browsers:


Screenshot from caniuse.com
Screenshot from caniuse.com.

So yeah, you should be good to go for the most part, unless you need to support IE9 or older. SVG filter support is relatively stable, and is more widespread than CSS filters and blend modes. There are also few major weird bugs, unlike with CSS blend modes (where only Chrome 46 has issues rendering the multiply, difference, and exclusion blend modes).

Note: Some of the 3-D filters, such as feConvolveMatrix, do have known bugs in certain browsers, though feColorMatrix, which this article focuses on, does not. Also, keep in mind that performance will inevitably take a tiny hit when it comes to applying any action in-browser (as opposed to rendering a pre-edited image on the page).

Using filters#section3

The basic layout of an SVG filter goes like this:


<svg>
  <filter id="filterName">
    // filter definition here can include
    // multiple of the above items
  </filter>
</svg>

Within an SVG, you can declare a filter. Most of the time, you’ll want to declare filters within defs of an SVG and can apply them in CSS like so:


.filter-me {
  filter: url('#filterName');
}

The filter URL is relative, so both filter: url('../img/filter.svg#filterName') and filter: url('http://una.im/filters.svg#filterName') are valid.

feColorMatrix#section4

When it comes to color manipulation, feColorMatrix is your best option. feColorMatrix is a filter type that uses a matrix to affect color values on a per-channel (RGBA) basis. Think of it like editing the channels in Photoshop.

This is what the feColorMatrix looks like (with each RGBA value as 1 by default in the original image):


<filter id="linear">
    <feColorMatrix
      type="matrix"
      values="R 0 0 0 0
              0 G 0 0 0
              0 0 B 0 0
              0 0 0 A 0 "/>
  </filter>
</feColorMatrix>

The matrix here is actually calculating a final RGBA value in its rows, giving each RGBA channel its own RGBA channel. The last number is a multiplier. The final RGBA value can be read from top to bottom like a column:


/* R G B A 1 */
1 0 0 0 0 // R = 1*R + 0*G + 0*B + 0*A + 0
0 1 0 0 0 // G = 0*R + 1*G + 0*B + 0*A + 0
0 0 1 0 0 // B = 0*R + 0*G + 1*B + 0*A + 0
0 0 0 1 0 // A = 0*R + 0*G + 0*B + 1*A + 0

Here’s a better visualization:


Hand-drawn sketch showing a schematic visualization of the fecolormatrix

RGB values#section5

Colorizing#section6

You can colorize images by omitting and mixing color channels like so:


<!-- lacking the B & G channels (only R at 1) -->
<filter id="red">
  <feColorMatrix
    type="matrix"
    values="1   0   0   0   0
            0   0   0   0   0
            0   0   0   0   0
            0   0   0   1   0 "/>
</filter>

<!-- lacking the R & G channels (only B at 1) -->
<filter id="blue">
 <feColorMatrix
    type="matrix"
    values="0   0   0   0   0
            0   0   0   0   0
            0   0   1   0   0
            0   0   0   1   0 "/>
</filter>

<!-- lacking the R & B channels (only G at 1) -->
<filter id="green">
  <feColorMatrix
    type="matrix"
    values="0   0   0   0   0
            0   1   0   0   0
            0   0   0   0   0
            0   0   0   1   0 "/>
</filter>

Here’s what adding the “green” filter to an image looks like:


Photo showing what the addition of the “green” filter would look like

Channel mixing#section7

You can also mix RGB channels to get solid colorizing results:


<!-- lacking the B channel (mix of R & G)
Red + Green = Yellow
This is saying there is no yellow channel
-->
<filter id="yellow">
  <feColorMatrix
    type="matrix"
    values="1   0   0   0   0
            0   1   0   0   0
            0   0   0   0   0
            0   0   0   1   0 "/>
</filter>

<!-- lacking the G channels (mix of R & B)
Red + Blue = Magenta
-->
<filter id="magenta">
  <feColorMatrix
    type="matrix"
    values="1   0   0   0   0
            0   0   0   0   0
            0   0   1   0   0
            0   0   0   1   0 "/>
</filter>

<!-- lacking the R channel (mix of G & B)
Green + Blue = Cyan
-->
<filter id="cyan">
  <feColorMatrix
    type="matrix"
    values="0   0   0   0   0
            0   1   0   0   0
            0   0   1   0   0
            0   0   0   1   0 "/>
</filter>

In each of the previous examples, we mixed colors in CMYK mode, so removing the red channel would mean that green and blue remain. When green and blue mix, they create cyan. Red and blue make magenta. We still retain some of the red and blue values where they are most prominent, but in areas that lack the two (light areas of white, where all colors are present in the RGB schema, or areas of green), the RGBA values of the other two channels replace them.

Justin McDowell has written an excellent article that explains HSL (hue, saturation, lightness) color theory. With SVG, the lightness value is the luminosity, which we also need to keep in mind. Here, each luminosity level is retained in each channel, so for magenta, we get an image that looks like this:


Photo showing how a magenta effect is produced when each luminosity level is retained in each channel

Why is there so much magenta in the clouds and lightest values? Consider the RGB chart:

RGB chart

When one value is missing, the other two take its place. So now, without the green channel, there is no white, cyan, or yellow. These colors don’t actually disappear, however, because their luminosity (or alpha) values have not yet been touched. Let’s see what happens when we manipulate those alpha channels next.

Alpha values#section8

We can play with the shadow and highlight tones via the alpha channels (fourth column). The fourth row affects overall alpha channels, while the fourth column affects luminosity on a per-channel basis.


<!-- Acts like an opacity filter at .5 -->
<filter id="alpha">
  <feColorMatrix
    type="matrix"
    values="1   0   0   0   0
            0   1   0   0   0
            0   0   1   0   0
            0   0   0   .5  0 "/>
</filter>

<!-- increases green opacity to be
     on the same level as overall opacity -->
<filter id="hard-green">
  <feColorMatrix
    type="matrix"
    values="1   0   0   0   0
            0   1   0   1   0
            0   0   1   0   0
            0   0   0   1   0 "/>
</filter>

<filter id="hard-yellow">
  <feColorMatrix
    type="matrix"
    values="1   0   0   1   0
            0   1   0   1   0
            0   0   1   0   0
            0   0   0   1   0 "/>
</filter>

In the following example, we’re reusing the matrix from the magenta example and adding a 100% alpha channel on the blue level. We retain the red values, yet override any red in the shadows so the shadow colors all become blue, while the lightest values that have red in them become a mix of blue and red (magenta).


<filter id="blue-shadow-magenta-highlight">
  <feColorMatrix
    type="matrix"
    values="1   0   0   0   0
            0   0   0   0   0
            0   0   1   1   0
            0   0   0   1   0 "/>
</filter>


Image showing what happens when we reuse the matrix from the magenta example and add a 100% alpha channel on the blue level

If this last value was less than 0 (up to -1), the opposite would happen. The shadows would turn red instead of blue. At -1, these create identical effects:


<filter id="red-overlay">
  <feColorMatrix
    type="matrix"
    values="1   0   0   0   0
            0   0   0   0   0
            0   0   1  -1   0
            0   0   0   1   0 "/>
</filter>

<filter id="identical-red-overlay">
  <feColorMatrix
    type="matrix"
    values="1   0   0   0   0
            0   0   0   0   0
            0   0   0   0   0
            0   0   0   1   0 "/>
</filter>


Image showing a red overlay, making the shadows red instead of blue

Making this value .5 instead of -1, however, allows us to see the mixture of color in the shadow:


<filter id="blue-magenta-2">
  <feColorMatrix
    type="matrix"
    values="1   0   0   0   0
            0   0   0   0   0
            0   0   1  .5   0
            0   0   0   1   0 "/>
</filter>


Image showing a mixture of colors in the shadows

Blowing out channels#section9

We can affect the overall alpha of individual channels via the fourth row. Since our example has a blue sky, we can get rid of the sky and the blue values by converting blue values to white, like this:


<filter id="elim-blue">
  <feColorMatrix
    type="matrix"
    values="1   0   0   0   0
            0   1   0   0   0
            0   0   1   0   0
            0   0   -2   1   0 "/>
</filter>


Image showing an example of blowing out a channel. We can get rid of the sky and the blue values by  converting blue values to white

Here are a few more examples of channel mixing:


<!-- No G channel, Red is at 100% on the G Channel, so the G channel looks Red (luminosity of G channel lost) -->
<filter id="no-g-red">
  <feColorMatrix
    type="matrix"
    values="1   1   0   0   0
            0   0   0   0   0
            0   0   1   0   0
            0   0   0   1   0 "/>
</filter>

<!-- No G channel, Red and Green is at 100% on the G Channel, so the G Channel looks Magenta (luminosity of G channel lost) -->
<filter id="no-g-magenta">
  <feColorMatrix
    type="matrix"
    values="1   1   0   0   0
            0   0   0   0   0
            0   1   1   0   0
            0   0   0   1   0 "/>
</filter>

<!-- G channel being shared by red and blue values. This is a colorized magenta effect (luminosity maintained) -->
<filter id="yes-g-colorized-magenta">
  <feColorMatrix
    type="matrix"
    values="1   1   0   0   0
            0   1   0   0   0
            0   1   1   0   0
            0   0   0   1   0 "/>
</filter>

Lighten and darken#section10

You can create a darken effect by setting the RGB values at each channel to a value less than 1 (which is the full natural strength). To lighten, increase the values to greater than 1. You can think of this as expanding or decreasing the RGB color circle shown earlier. The wider the radius of the circle, the lighter the tones created and the more white is “blown out”. The opposite happens when the radius is decreased.


Diagram showing how you can create a darken effect by setting the RGB values at each channel to a a value less than 1; to lighten, increase the values to greater than 1

Here’s what the matrix looks like:


<filter id="darken">
  <feColorMatrix
    type="matrix"
    values=".5   0   0   0   0
             0  .5   0   0   0
             0   0  .5   0   0
             0   0   0   1   0 "/>
</filter>


Image with a darken filter applied

<filter id="lighten">
  <feColorMatrix
    type="matrix"
    values="1.5   0   0   0   0
            0   1.5   0   0   0
            0   0   1.5   0   0
            0   0   0   1   0 "/>
</filter>


Image with a lighten filter applied

Grayscale#section11

You can create a grayscale effect by accepting only one shade’s pixel values in a column. There are different grayscale effects, however, based on which active levels one applies. Here we’re doing a channel manipulation, since we’re grayscaling the image. Consider these examples:


<filter id="gray-on-light">
  <feColorMatrix
    type="matrix"
    values="1   0   0   0   0
            1   0   0   0   0
            1   0   0   0   0
            0   0   0   1   0 "/>
</filter>


Image showing a 'gray on light' effect

<filter id="gray-on-mid">
  <feColorMatrix
    type="matrix"
    values="0   1   0   0   0
            0   1   0   0   0
            0   1   0   0   0
            0   0   0   1   0 "/>
</filter>


Image showing a 'gray on mid' effect

<filter id="gray-on-dark">
  <feColorMatrix
    type="matrix"
    values="0   0   1   0   0
            0   0   1   0   0
            0   0   1   0   0
            0   0   0   1   0 "/>
</filter>


Image showing a 'gray on dark' effect

Pulling it all together#section12

The real power of feColorMatrix lies in its ability to mix channels and combine many of these concepts into new image effects. Can you read what’s going on in this filter?


<filter id="peachy">
  <feColorMatrix
    type="matrix"
    values="1   0   0   0   0
            0  .5   0   0   0
            0   0   0  .5   0
            0   0   0   1   0 "/>
</filter>

We’re using the red channel at its normal alpha channel, applying green at half strength, and applying blue on the darker alpha channels but not at its original color location. The effect gives us dark blue in the shadows, and a mix of red and half-green for the highlights and midtones. If we recall red + green = yellow, red + (green/2) would be more of a coral color:


Image showing what happens when we use the red channel at its normal alpha channel, apply green at half strength, and apply blue on the darker alpha channels but not at its original color location

Here’s another example:


<filter id="lime">
  <feColorMatrix
    type="matrix"
    values="1   0   0   0   0
            0   2   0   0   0
            0   0   0  .5   0
            0   0   0   1   0 "/>
</filter>

In that segment, we’re using the normal pixel hue of red, a blown-out green, and blue devoid of its original hue pixels, but applied in the shadows. Again, we see that dark blue in the shadows, and since red + green = yellow, red + (green*2) would be more of a yellow-green in the highlights:


Image showing what happens when we use the normal pixel hue of red, a blown-out green, and blue devoid of its original hue pixels, but applied in the shadows. Again, we see that dark blue in the shadows, and since red + green = yellow, red + (green*2) would be more of a yellow-green in the highlights

So much can be explored by playing with these values. An excellent example of such exploration is Rachel NaborsDev Tools Challenger, where she filters out the longer wavelengths (i.e., the red and orange channels) from the fish in the sea, explaining why “Orange Roughy” actually appears black in the water. (Note: requires Firefox.)

How cool! Science! And color filters! Now that you have a basic grasp of the situation, you, too, have the tools you need to create your own effects.

For some of those really rad Spotify duotone effects, I recommend you check out an article by Amelia Bellamy-Royds, who goes into even more detail about feColorMatrix. Sara Soueidan also wrote an excellent post on image effects where she recreates CSS blend modes with SVG.

Filter effects reference#section13

Once you understand what’s going on with the feColorMatrix, you have the basic tools to create detailed filters within a single contained filter definition, but there are other options out there that will let you take it even further. Here’s a handy guide to all of the fe-* options currently out there for further exploration:

  • feBlend: similar to CSS blend modes, this function describes how images interact via a blend mode
  • feComponentTransfer: an umbrella term for a function that alters individual RGBA channels (i.e. , feFuncG)
  • feComposite: a filter primitive that defines pixel-level image interactions
  • feConvolveMatrix: this filter dictates how pixels interact with their close neighbors (i.e., blurring or sharpening)
  • feDiffuseLighting: defines a light source
  • feDisplacementMap: displaces an image (in) using the pixel values of another input (in2)
  • feFlood: complete fill of the filter subregion with a specified color and alpha level
  • feGaussianBlur: blurs input pixels using an input standard deviation
  • feImage: for use within other filters (like feBlend or feComposite)
  • feMerge: allows for asynchronous application of filter effects, instead of layering them
  • feMorphology: erodes or dilates lines of source graphic (think strokes on text)
  • feOffset: used for creating drop shadows
  • feSpecularLighting: source for the alpha component as a bump map, a.k.a. the “specular” portion of the Phong Reflection Model
  • feTile: refers to how an image is repeated to fill a space
  • feTurbulence: allows the creation of synthetic textures using Perlin Noise

Additional resources#section14

6 Reader Comments

  1. Una, I saw you demonstrate filters in NY and it was amazing to watch!

    And yes, SVG filters give you full customisation so you can build the exact filter you want. And apply it to HTML via CSS, of course.

    feColorMatrix is cool but it is linear; each channel in the output is a weighted sum of channels in the input. Or in Photoshop terms, there is no curve.

    To get a colorization effect, for example, you want black to stay black and white to stay white (or some pastel color, perhaps) but a per-component non-linear transfer curve between those. The differences in the curves are what gives you the color. So feComponentTransfer is your freind here (quite possibly combined with some feColorMatrix first, to set up the colors you want).

    I just wondered if you had experimented with that, because I am sure you would get some awesome results.

  2. Yes! Thanks for the comment 🙂

    (for those curious): feComponentTransfer is a great way to affect individual RGBA channel functions via feFuncR, feFuncG, feFuncB, and feFuncA. These are individual functions acting within feComponent and don’t act in the same way that feColorMatrix will “average out” a new RGBA model to map pixels to (see diagram above).

    For more info, there’s an excellent post on this in the Web Platform Docs: https://docs.webplatform.org/wiki/svg/elements/feComponentTransfer and the article I linked above “Smarter SVG Filters” also shows this technique. For those looking to dive deeper into the various color functions, check out the spec here: https://www.w3.org/TR/2003/REC-SVG11-20030114/filters.html#feFuncRElement

  3. Hey there.

    IE10+ and Edge support is a bit misleading since SVG filters only work on SVG elements in these browsers. Because this article is primarily about images, you can fallback to SVG markup for and have support for IE10+ and Edge as well as all other modern browsers.

    Yes it might be a little bit ugly and you have to control the size of you image manually, but it works.

    Demo:
    https://jsfiddle.net/p73938w8/
    https://jsfiddle.net/p73938w8/show/

  4. Great article!! As usual from both ALA and Una!!

    What if I want to change a specific colour (e.g.: #fafee2) to another one? Or set that specific colour to have Alpha=0?

    Is that still possible without using conditionals?

Got something to say?

We have turned off comments, but you can see what folks had to say before we did so.

More from ALA