Tab Completion

I'm Tab Atkins Jr, and I wear many hats. I work for Google on the Chrome browser as a Web Standards Hacker. I'm also a member of the CSS Working Group, and am either a member or contributor to several other working groups in the W3C. You can contact me here.
Listing of All Posts

Doing an Inset Drop-shadow With SVG Filters

Last updated:

Using SVG Filters can be really powerful, but also pretty complicated. In particular, SVG can do drop shadows better than CSS's box-shadow property, because it can respond to the transparency of whatever you're doing, rather than only shadowing the element's box.

However, the drop-shadow filter is not a primitive: it's built up out of the simpler filter effects, and it's fairly complex:

 <filter id="drop-shadow">
  <feGaussianBlur in="[alpha-channel-of-input]" stdDeviation="[radius]"/>
  <feOffset dx="[offset-x]" dy="[offset-y]" result="offsetblur"/>
  <feFlood flood-color="[color]"/>
  <feComposite in2="offsetblur" operator="in"/>
  <feMerge>
    <feMergeNode/>
    <feMergeNode in="[input-image]"/>
  </feMerge>
</filter> 

This only does a plain drop-shadow, where the element is casting a shadow below it. Modifying it to do an inset shadow, where the element is "cut out" and the light source is casting a shadow into it, isn't hard, but you have have to know what all the little bits of the original shadow are doing.

Let's get the important bit out of the way. Here's a working inset drop-shadow with SVG filters:

And here's the code for it:

<!DOCTYPE html>
<svg style="border: thin solid;">
   <filter id="inset-shadow" x="-50%" y="-50%" width="200%" height="200%">
    <feComponentTransfer in=SourceAlpha>
      <feFuncA type="table" tableValues="1 0" />
    </feComponentTransfer>
    <feGaussianBlur stdDeviation="3"/>
    <feOffset dx="5" dy="5" result="offsetblur"/>
    <feFlood flood-color="rgb(20, 0, 0)" result="color"/>
    <feComposite in2="offsetblur" operator="in"/>
    <feComposite in2="SourceAlpha" operator="in" />
    <feMerge>
      <feMergeNode in="SourceGraphic" />
      <feMergeNode />
    </feMerge>
  </filter> 
  <circle cx=50 cy=50 r=20 fill=red filter="url(#inset-shadow)" />
</svg>

Let's walk through this, node by node.

  1. First, set up the filter region with the x/y/width/height attributes. By default, the entire filtering operation takes place in a box 10% larger in every direction than the source graphic. Since we're working with blurs, which can extend a good bit away from the source, we need a bit more spaaaaaace. 50% larger in all directions is generally fine.

  2. <feComponentTransfer> We start by taking the alpha channel of the source graphic with in="SourceAlpha", and inverting it. The plain drop-shadow uses the graphic's normal alpha channel, blurring and offsetting it to produce the shadow. Here, the source graphic itself is not casting the shadow - everything else is - so we need to invert its alpha. This gives us an image that is transparent where the image is solid, and solid where the image is transparent, which we'll turn into the shadow.

  3. <feGaussianBlur> Now we blur that image, preparing it for shadow-ness, and then use <feOffset> to shift it a bit, so the light source will appear to come from the side a bit.

    This is also the first time we see the result attribute. By default, each sibling filter effect takes the output of the previous filter effect as its input. If you need do something complicated, though, you can name the output of a particular filter effect, and refer to it explicitly later.

  4. The next two elements are somewhat non-obvious. When we grabbed the "SourceAlpha", it gave us a solid-black image with the same alpha channel as the source image. If we want a black shadow, that works fine, but if we want to color it, we need to do some work. In this case, we use <feFlood> to generate an infinite field of color (my example just uses black again, whatever), and then, the important bit, uses <feComposite operator="in"> to chop it down to just the bits that overlap the blurred shadow.

    There are a bunch of composite operations, but "in", in particular, basically multiplies the alpha channel of the first image (in this case, implicitly the result of the previous <feFlood> effect) with the alpha channel of the second image (explicitly pointing to the offsetblur result). It's like using a stencil, where the solid parts of the in2 image let the in image show through, and the transparent parts block it.

    This gives us a blur of the right color. If you just want a black shadow, you can drop this <feFlood> and the first <feComposite>, as the blur you generated previously starts out black.

  5. We then do another <feComposite operator="in">, this time using the SourceAlpha again as the stencil mask. This cuts out most of the blur, leaving us with only the parts that will overlap the source image.

  6. Finally, we merge the shadow and the source image with <feMerge> and the predefined SourceImage value, putting the shadow second so it goes on top.

And we're done! When you reference this filter, it produces an inset drop shadow over your image.

(a limited set of Markdown is supported)

#1 - Brad Kemper:

Nice. Do you know why it doesn't work in Safari? Also, what would you add for spread (or choke, which is spread with a negative number)?

Reply?

(a limited set of Markdown is supported)

It's probably the same as the Firefox bug, where some optimization to automatically trim the filter region down to just the bounding box of the non-transparent pixels isn't being applied correctly. In particular, it doesn't realize that a <feComponentTransfer><feFuncA> can map 0-alpha pixels to non-zero values, so <feComponentTransfer> ends up not adjusting the auto region at all.

I think spread/choke can be done similarly to this cool "shape blobbing" technique - blur a shape, then do a 'discrete' component transfer to send all pixels above/below a cutoff to fully solid or fully transparent. Then you can blur it as normal. I'd have to play with this.

Obviously, such a technique would round corners (or sharpen them in a choke).

Reply?

(a limited set of Markdown is supported)

Re #2: Hi, thanks for showing me how to invert the alpha channel without the use of the color matrix!

Regarding the bug: I replaced <feComponentTransfer> with <feFlood /> <feComposite in2="SourceAlpha" operator="out"/> filling the extended area with black excluding the alpha of our input image. Now it looks well in Firefox, too.

Reply?

(a limited set of Markdown is supported)

Re #3:

This inset shadow seems to cause issues with gradient banding, for example:

Ive got one SVG with the inset shadow applied: https://daks2k3a4ib2z.cloudfront.net/57fa771f45bf29a4636b6816/584a6439a2cecbb5440a23a5_molecule%20background%20top%20(no%20opacity%20mask).svg

And another one without the inset shadow: https://daks2k3a4ib2z.cloudfront.net/57fa771f45bf29a4636b6816/584a65c0a2cecbb5440a24c2_molecule%20background%20top%20(no%20opacity%20mask%20or%20shadow).svg

You can see the first one has banding in the gradient from orange to black. Any way to fix this so it has a smooth gradient like the second one?

Reply?

(a limited set of Markdown is supported)

How about flood-color="rgba(0,0,0,0.5)" ? This is imho easier to maintain if object's color changes later or something should "emit light" from the shadowed area, like a capital city on the night side of the Earth globe.

Reply?

(a limited set of Markdown is supported)