Have you used filter: blur(), and were disappointed with the soft halo around the filtered element? Would you like to fix that?

The neat thing about CSS filters is they’re defined with SVG’s <filter>, which means we can tweak them. The Filter Effects spec defines filter: blur() as this:

  <filter id="blur">
  <feGaussianBlur stdDeviation="[radius radius]"/>
</filter> 

We’ll replace that with this:

  <filter id="better-blur" x="0" y="0" width="1" height="1">
  <feGaussianBlur stdDeviation="[radius radius]" result="blurred"/>

  <feMorphology in="blurred" operator="dilate" radius="[radius radius]" result="expanded"/>

  <feMerge>
    <feMergeNode in="expanded"/>
    <feMergeNode in="blurred"/>
  </feMerge>
</filter>

UPDATE: <feMorphology> has some issues. See the comments for alternatives; Vincent de Oliveira’s is particularly good.

With the optimized SVG data: URI technique, the filter can be embedded in the CSS as a paltry 308 bytes:

  filter: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='a' x='0' y='0' width='1' height='1'%3E%3CfeGaussianBlur stdDeviation='4' result='b'/%3E%3CfeMorphology operator='dilate' radius='4'/%3E %3CfeMerge%3E%3CfeMergeNode/%3E%3CfeMergeNode in='b'/%3E%3C/feMerge%3E%3C/filter%3E%3C/svg%3E#a");

Notice the #a at the end with the unencoded hash sign. We want that unencoded, because it references the <filter>’s id.

Custom CSS filters work in most browsers… that aren’t Internet Explorer or Edge. (Edge does support filter shorthands.) Safari won’t accept url() filters unless they reference a fragment in the HTML markup, like filter: url(#foo).

There are a couple ways to deal with those spoilsports:

  1. Put <filter id="better-blur"> and <svg><image filter="url(#better-blur)"/></svg> in the markup.

  2. If that’s gross and you want pure CSS, try this filter fallback.

Better blurring with Sass

Here’s a Sass mixin that outputs our new filter with any amount of blurring:

  @mixin better-blur($radius) {
  filter: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='a' x='0' y='0' width='1' height='1'%3E%3CfeGaussianBlur stdDeviation='#{$radius}' result='b'/%3E%3CfeMorphology operator='dilate' radius='#{$radius}'/%3E %3CfeMerge%3E%3CfeMergeNode/%3E%3CfeMergeNode in='b'/%3E%3C/feMerge%3E%3C/filter%3E%3C/svg%3E#a");
}

How does it work?

So, you’ve read this far, have you? Good, good. There are 2 changes to the default blur filter.

Smaller filtered area

If you omit the x, y, width, and height attributes from <filter>, they default to the following:

  <filter x="-10%" y="-10%" width="120%" height="120%">
</filter>

The official blur() shorthand filter does omit those attributes. That’s how the element’s blur expands beyond its bounding box.

These attributes work like opacity, where they accept a percentage, or a number from 0 to 1 that does the same thing. By setting x and y to 0, and width and height to 1, the filtered area matches the element’s dimensions.

Overlay the blurred element on top of an expanded version of itself

  <feGaussianBlur stdDeviation="[radius radius]" result="blurred"/>

<feMorphology in="blurred" operator="dilate" radius="[radius radius]" result="expanded"/>

<feMerge>
  <feMergeNode in="expanded"/>   
  <feMergeNode in="blurred"/>
</feMerge>

I’m going to be real: I’m not sure how this works. But it seems to. Without it, there is a noticeable fade near the element’s edges.

I do know that <feGaussianBlur> is not supposed to have that white blur in the first place, since an omitted edgeMode attribute defaults to edgeMode="duplicate". So, uh, I’m stumped. Browsers work in mysterious ways.


3,946 10 24