Fixing the white glow in the CSS blur() filter
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 filter
s 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:
Put
<filter id="better-blur">
and<svg><image filter="url(#better-blur)"/></svg>
in the markup.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.