The following is a guest post by Emil Björklund. Filter effects in CSS have been around for a while, and together with things like blend modes, they bring new possibilities for recreating and manipulating stuff in the browser that we previously had to do in Photoshop. Here, Emil explores a performance technique using one of the more forgotten filter effects – the filter function – as well as recreating it with SVG.
This all starts with a post from the Facebook engineering team on how they load cover photo previews in their native apps. The problem they faced is that these “cover photos” are large and often take a while to load, leaving the user with a less-than-ideal experience when the background suddenly changes from a solid color to an image.
This is especially true on low-connectivity or mobile networks, which often leave you staring at an empty gray box as you wait for images to download.
Ideally the image would be encoded into the initial API response of their app when getting the profile data. But to fit inside this request, the image would have to be capped at 200 bytes. Troublesome, as cover photos are over 100 kilobytes in size.
So how do you get something valuable out of 200 bytes and how do we show the user something before the image is fully loaded?
The (ingenious) solution was to return a tiny image (around 40 pixels wide) and then scale that tiny image up whilst applying a gaussian blur. This instantly shows a background that looks aesthetically pleasing, and gives a preview of how the cover image would look. The actual cover image could then be loaded in the background in good time, and smoothly switched in. Smart thinking!
There are a couple of cool things about this technique:
- It makes the perceived loading time wicked fast.
- It uses something traditionally costly in terms of performance to increase performance.
- It’s doable on the web.
Big header background images (and their performance drawbacks) are definitely something that we can relate to when building for the web, so this is useful stuff. We may try to avoid the downloading of heavy images, but sometimes we make the tradeoff to to achieve a certain mood. The best we can do in that situation is try to optimize the perceived performance, so we might as well steal this technique.
A working example
We’re going to recreate this header image feature with a sort of “critical CSS” approach. The very first request will load the tiny image in inline CSS, then the high-res background comes after first render.
It will look something like this when it’s done loading:

In this example, we are using a background image, regarding it as decoration rather than part of the content. There are some finer points to debate around when these types of images are to be regarded as content (and thus coded as an <img>
) and when they are background images. In order to make use of smart sizing modes (like the CSS values cover
and contain
), background images are probably the most common solution for these kinds of designs, but new properties like object-fit
are making the same approach a bit easier for content images. Sites like Medium already use blurred content images to improve load times, but the usefulness of that technique is debatable – do the blurred images bring anything to the table if the loading technique fails? Anyway: in this article, we’ll focus on this technique as applicable to background images.
Here’s the outline of how it’ll work:
- Inline a tiny image preview (40×22 pixels) as a base64-encoded background image inside of a
<style>
-tag. The style tag also includes general styling and the rules for applying a gaussian blur to the background image. Finally, it includes styles for the larger version of the header image, scoped to a different class name. - Get the URL to the large image from the inline CSS, and preload it using JavaScript. If the script fails for some reason, no harm no foul – the blurred background image is still there, looking pretty cool.
- When the large image is loaded, add a class name that toggles the CSS to use the large image as the background while removing the blur. Hopefully, the blur-removal part can also be animated.
You can find the final example as a Pen. You’ll likely see the blurred image for a moment before the sharper image loads. If not, try reloading the page with an empty cache.
A tiny, optimized image
First of all, we need a preview version of the image. Facebook got the size of theirs down to 200 bytes via compression voodoo (like storing the non-changing JPEG header bits in the app), but we’re not going to get quite that extreme. With a size of 40 by 22 pixels, this particular image comes in at around 1000 bytes after running it through some image optimization software.

The full-size JPEG image is around 120Kb at 1500 × 823 pixels. That file size could probably be a lot lower, but we’ll leave that as is, since this is a proof of concept. In a real-world example, you would probably have a few size variations of the image and load a different one based on viewport size – heck, maybe even load a different format like WebP.
filter
function for images
The Next, we want to scale the tiny image up to cover the element, but we don’t want it looking pixelated and ugly. This is where the filter()
-function comes in. Filters in CSS might seem a little confusing, as there are effectively three kinds: the filter
property, its proposed backdrop-filter
counterpart (in the Filter Effects Level 2 spec) and finally the filter()
function for images. Let’s take a look at the property first:
.myThing {
filter: hue-rotate(45deg);
}
One or more filters are applied, each operating on the result of the previous – a lot like a list of transforms. There’s a whole range of predefined filters that we can use: blur()
, brightness()
, contrast()
, drop-shadow()
, grayscale()
, hue-rotate()
, invert()
, opacity()
, sepia()
and saturate()
.
What’s even cooler is that this is a spec shared between CSS and SVG, so not only are the predefined filters specced in terms of SVG, we can also create our own filters in SVG and reference them from CSS:
.myThing {
filter: url(myfilter.svg#myCustomFilter);
}
The same filter effects are valid in backdrop-filter
, applying them when compositing a transparent element with its backdrop – perhaps most useful for creating the “frosted glass” effect.
Finally, there’s the filter()
function for image values. The idea is that anywhere that you can reference an image in CSS, you should also be able to pipe it through a list of filters. For the tiny header image, we inline it as a base64 dataURI and run it through the blur()
filter.
.post-header {
background-image: filter(url(data:image/jpeg;base64,/9j/4AAQ ...[truncated] ...), blur(20px));
}
This is great, since this is exactly what we’re looking for when recreating the technique from the Facebook app! There’s bad news on the support front though. The filter
property is supported in the latest versions of all browsers except IE, but none of them except WebKit have implemented the filter()
function part of the spec.
When I say WebKit here, I mean the WebKit nightly builds at the time this post is written, and not Safari. The filter
function for images is technically in iOS9 as -webkit-filter()
, but this hasn’t been reported anywhere official as far as I can find, which is a bit weird. The reason is probably that it has a horrible bug with background-size
: the original image is not resized, but the filtered output tile size is. This breaks background image functionality pretty bad, especially with blurring. It has been fixed, but not in time to make it into the Safari 9 release, so I guess they didn’t want to announce that feature.
But what do we do with the missing/broken filter()
functionality? We could either give browsers that don’t support it a solid background until the image loads, although that means they’ll get no background at all if JS fails to load. Boring!
No, we’ll save the filter()
function as an extra spice for animating the swapped-in image later, and instead emulate the filter function for the initial image with SVG.
Recreating the blur filter with SVG
Since the spec handily provides an SVG equivalent for the blur()
-filter, we can recreate how the blur filter works in SVG, with a few tweaks:
- The edges get a bit semi-transparent when applying the gaussian blur. We can fix this by adding something called a
feComponentTransfer
filter. The component transfer allows you to manipulate each color channel (including alpha) of a source graphic. This particular variation uses thefeFuncA
element, which maps any value between0
and1
in the alpha channel to1
, meaning it removes any alpha transparency. - The
color-interpolation-filters
attribute on the<filter>
element must be set tosRGB
. SVG filters default to using thelinearRGB
color space, and CSS operates insRGB
. Most browsers seem to handle the color corrections right, but Safari/WebKit makes the colors all washed out unless this value is set. - The
filterUnits
is set touserSpaceOnUse
, which in simplified terms means that coordinates and lengths (like thestdDeviation
of the blur) maps to pixels in the element we apply the blur to.
The resulting SVG code looks something like this:
<filter id="blur" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feGaussianBlur stdDeviation="20" edgeMode="duplicate" />
<feComponentTransfer>
<feFuncA type="discrete" tableValues="1 1" />
</feComponentTransfer>
</filter>
The filter
property uses its own url()
function where we can either reference, or URI-encode an SVG filter. So how do we apply a filter to to something inside of a background-image: url(...)
?
Well, SVG files can point to other images, and we can apply filters to those images inside the SVG. The problem is that SVG background images can’t fetch any outside resources. But we can get around this by base64-encoding the JPG inside the SVG. This wouldn’t be feasible with a large image, but for our tiny one it should be fine. The SVG will look like this:
<svg xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
width="1500" height="823"
viewBox="0 0 1500 823">
<filter id="blur" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feGaussianBlur stdDeviation="20 20" edgeMode="duplicate" />
<feComponentTransfer>
<feFuncA type="discrete" tableValues="1 1" />
</feComponentTransfer>
</filter>
<image filter="url(#blur)"
xlink:href="data:image/jpeg;base64,/9j/4AAQSkZJ ...[truncated]..."
x="0" y="0"
height="100%" width="100%"/>
</svg>
Another downside (compared to just using the filter()
-function with a bitmap) is that we’ll need to manually set some sizes for the SVG to cooperate properly with the background size. The SVG itself has a viewBox
set to mimic the aspect ratio of the image, and the width
and height
properties are set to the same measurements to make sure it works cross-browser (for example, IE screws up the aspect ratio if these are missing). Finally, the <image>
-element is set to cover the entire SVG canvas.
Now we can use this file as a background to the post header, and it’ll look something like this:

As a final step, we can put the SVG wrapper image inline in the CSS to avoid an extra request. Inline SVG needs to be URI encoded, I use yoksel’s SVG encoder for this. So now we have a dataURI containing another dataURI. DataURInception!
When encoding the SVG we get some text to paste into the url()
, but it’s worth noting that we need to prepend some metadata to make it display: data:image/svg+xml;charset=utf-8,
. The charset
-stuff is important: it makes the encoded SVG play nicely across browsers.
.post-header {
background-color: #567DA7;
background-size: cover;
background-image: url(data:image/svg+xml;charset=utf-8,%3Csvg...);
}
At this point, the whole page, including the image, is 1 request and 5KB when using GZIP.
Getting the URL to the big image
Next, we create a rule for the enhanced header, where we set up the huge background image.
.post-header-enhanced {
background-image: url(largeimg.jpg);
}
Instead of just toggling the class name, thus triggering the big image to load, we want to preload the big image and then apply the class name. This is so that we can smoothly animate the switch later, being reasonably sure the large image is done loading. Since we don’t want to hard-code the image URL in both the CSS and the JavaScript, we’ll grab the URL using JavaScript from inside the styles. As the class name is not yet applied, we can’t just look at headerElement.style.backgroundImage
etc – it doesn’t know about the background yet. To solve this, we’ll use the CSSOM – the CSS Object Model, and the read-only JS properties that let us traverse the CSS rules.
The following snippet finds the class name for the enhanced header, then grabs the URL with some regex. After that it preloads the image and triggers the added class name once that’s done.
<script>
window.onload = function loadStuff() {
var win, doc, img, header, enhancedClass;
// Quit early if older browser (e.g. IE 8).
if (!('addEventListener' in window)) {
return;
}
win = window;
doc = win.document;
img = new Image();
header = doc.querySelector('.post-header');
enhancedClass = 'post-header-enhanced';
// Rather convoluted, but parses out the first mention of a background
// image url for the enhanced header, even if the style is not applied.
var bigSrc = (function () {
// Find all of the CssRule objects inside the inline stylesheet
var styles = doc.querySelector('style').sheet.cssRules;
// Fetch the background-image declaration...
var bgDecl = (function () {
// ...via a self-executing function, where a loop is run
var bgStyle, i, l = styles.length;
for (i=0; i<l; i++) {
// ...checking if the rule is the one targeting the
// enhanced header.
if (styles[i].selectorText &&
styles[i].selectorText == '.'+enhancedClass) {
// If so, set bgDecl to the entire background-image
// value of that rule
bgStyle = styles[i].style.backgroundImage;
// ...and break the loop.
break;
}
}
// ...and return that text.
return bgStyle;
}());
// Finally, return a match for the URL inside the background-image
// by using a fancy regex I Googled up, as long as the bgDecl
// variable is assigned at all.
return bgDecl && bgDecl.match(/(?:\(['|"]?)(.*?)(?:['|"]?\))/)[1];
}());
// Assign an onLoad handler to the dummy image *before* assigning the src
img.onload = function () {
header.className += ' ' +enhancedClass;
};
// Finally, trigger the whole preloading chain by giving the dummy
// image its source.
if (bigSrc) {
img.src = bigSrc;
}
};
</script>
The script quits early if addEventListener
is not supported, which should overlap nicely with the rest of the support needed. As far as I can tell, all reasonably modern SVG-supporting browsers support the rest of the CSSOM and other JavaScript features used.
Animating the swap
It’s a bit of a bummer that we didn’t get to use the filter()
-function, after finding out that it exists and all. So we’ll add an animated effect, when swapping in the high-res image. This only works in WebKit nightlies at the moment, and we can safely use the @supports
-rule to scope the changes. Here’s an animated GIF to show the effect in action:

Note that we can’t use a transition
for this: the filter()
-function is animatable, but only for changing values in the filter chain – when the background image changes, we’re out of luck. We can, however, use an animation for this, but it does mean that we need to repeat the URL to the background image two more times, as the start and end values. A small price to pay.
Here’s the CSS for the enhanced header styles for browsers that do understand the filter()
-function:
@supports (background-image: filter(url('i.jpg'), blur(1px))) {
.post-header {
transform: translateZ(0);
}
.post-header-enhanced {
animation: sharpen .5s both;
}
@keyframes sharpen {
from {
background-image: filter(largeimg.jpg), blur(20px));
}
to {
background-image: filter(largeimg.jpg), blur(0px));
}
}
}
One final detail is the translateZ(0)
-trick on the header here: without it, the animation is crazy jerky. I tried being all modern and used will-change: background-image
, but that didn’t persuade the browser to create a hardware-backed layer, so I had to go with the old trick of adding a 3D “null transform”.
Fast, progressively-enhanced background images
There we have it, a page with a humongous background image (albeit blurry) loading in 5Kb, lazy-loading the sharp looking full-size image. Right now, only WebKit can animate the sharper image in, but I’m hopeful that other browsers will implement the filter()
-function soon. I’m sure there’s lots more fun techniques we can use it for.
One word for you: epic. Thank you!
Very cool.
What are your thoughts on doing it all in CSS?
Something like this…
Thanks!
I’m not exactly sure what you mean by ”all in CSS” and what your code is supposed to do differently – maybe you could clarify that a little? I’m all for making more stuff using CSS. :-)
Hi Emil,
Sorry realise that wouldn’t work, it’s been a long day, but still think it’s possible.
If I understand it correctly it looks like you’re using JS to preload the large image, then add a class to the element to animate the image into place.
Here I’m loading the small image inline as you are.
Then after 1 seconds I preload the large image with animation.
Then after 5 seconds fade it in. (Times are exaggerated for effect, not sure what optimum would be).
Cheers,
Mark
Ah, gotcha. Yeah, that’s pretty cool, I guess the only issue would be that it would be a “guesstimate” of when the big image is done loading. I guess my idea was “load something insanely fast and then use JS to decide what to do next”, which could be generalized into other solutions – image formats, not loading the big image at all…
Ah yeah makes sense, can’t think of a way to detect that in CSS alone.
I’m an email dev so always after JS free solutions :)
Cheers
Nice post Emil! It got me thinking it would be cool using this technique for loading progressive JPEGs, toggling the blur on the
loadend
event. Have you given that any thought? Perhaps the overhead of doing the request + loading the very first part of the image would outweigh the minimal extra work of adding in a miniature image?Thanks, Simon! (and also hi! long time no see!)
That’s a very cool idea, but I’m guessing that it would only be useful for
<img>,
right? No load events for CSS background images… I’m sort of skeptical of using blur for content images, as the blurred image doesn’t add anything unless it loads properly, where a background image can still be decorative when blurred.How does this compare to a progressive JPEG?
Posts like this remind me how making lowsrc obsolete was a mistake.
Great post and clever! The effect raises web accessibility concerns for me:
I worry that blurring/focusing patterns (in general) could be a bit jarring for users that have vision or cognitive disabilities. For instance, if a user is already having trouble focusing on the content naturally, this could be even harder if the background is moving into focus as well… an almost depth of field effect.
The web is already blurry for many users out there and the jagged load of an image, even if it is progressive, may have less of an impact than a gradual blur to focus transition like this.
Because the effect requires JavaScript to feed the user the clear image, and because you can’t guarantee when the user’s browser will download and execute said JavaScript the effect could happen at any time… or maybe not at all. This could take the users eye off the content to wrestle the effect and then back to the content.
It matters less and less if the page loads fast if the user’s eye is battling focus.
Hi Joe, thanks for the feedback. You raise a really good point, and perhaps one that applies to a lot of different scenarios involving animation – it runs the risk of distracting and confusing. If there’s significant evidence that an animated focus-effect is bad for accessibility, I’d be happy to leave that part out in a “live” product.
That said, the animation isn’t crucial to the technique – it was more of a fun way to demonstrate what the
filter()
function can do. The “tiny image + blur = super fast first render” part is much more interesting in my opinion, plus “polyfilling” thefilter()
function in SVG.So, unless I’m missing something — once you’ve seen it become sharp once, you will never see it blurred again? That’s how the CodePen example runs anyway.
But I might be missing something :(
That’s ideal ;)
The image is cached and doesn’t require the blur up.
But as Emil says:
It’s because you already have the image on browser’s cache.
Try to open it on another browser or an incognito window.
I think this is a useful proof of concept, but I’d like to add a note:
If you’re working on a large, complicated website, you will often find resources blocking render, this includes all CSS and any sync JS in the head. The problem with this is that even your inline base64 tiny image will not even start to draw in the first few seconds in unfavorable conditions, so effectively users will still be looking at white space for a few seconds, then the blur image, almost immediately followed by the actual image.
This is not an error in your approach, I just want to emphasize that inlining CSS does not make it render instantly. That declaration still has to wait for ALL CSS on the page to be loaded and parsed. You could even inline a solid background color and it will not render before all CSS is loaded and parsed.
As such, this solution only makes sense on websites that are not render-blocking, and in my experience, few websites are that optimized.
Whilst at it, a second note: if you instead would use a foreground image, it may actually load faster than your current blur image. The reason for this is the look-ahead parser of the browser, which immediately starts the loading of said image, whilst your blur image is still waiting for all CSS on the page, as well as all blocking JS.
To summarize, actual reality may not be what many would expect:
Inlining does not mean instant rendering, it can still take seconds
Old-school foreground images may beat any CSS or JS-based solution
Mileage may vary, so be sure to look at the timelines for your situation.
One more small detail – it will be more precise and accurate if your big image and small one will have the same proportions, to avoid animation jumps and image moving during the transition. In my projects i’m using average background color to cover first stage of loading (before small image), but in your case (inline css) it’s not necessary.
How do you calculate the average color on a photo ?
Thanks !
Try using Color Theif. It’s a small JS library for getting the eg. the dominant color from an image.
Thank you for that !
For the small image you can use PNG, it will take less space than JPEG at tiny sizes.
Yeah, I experimented quite a bit with various formats, but JPEG was still the most “bang for the buck” for this one, even at the smaller size (and after image optimization).
Not necessarily. And in this case, No.
The tiny image (40×22) in PNG weighs 1.78KB. In JPG, 473B at 35% quality.
If the image used less and flatter colors, that’s certainly a possibility.
NOTE: make sure you performance test, especially on mobile. Android devices have relatively crappy CSS performance, especially before Android v5. Blurring a full width background image will affect render speed, so make sure you know the trade offs before applying.
That said, if the device can handle it, the effect is awesome :)
Great article! I recently post about the technique used by Medium to do this. In their case they use a canvas to apply the blur() effect.
Regarding the tiny, optimized images, if using webp is an option I strongly recommend having a look at it. WebP with the highest compression setting produces images that are 75% smaller than the equivalent using JPG at the maximum compression, at least when applied to these tiny images. I did some research on https://jmperezperez.com/webp-placeholder-images/.
If you use this with images in the body, as opposed to just the header, it means that if you save something to read offline (in Safari reading list, for example), the images will never actually load. Please, think twice before using this technique, especially if the images are crucial to the understanding of the content.
Nice, I wonder how performant it is on lower specced devices though, I imagine that animating the blur filter is a bit of a hog.
It might be more performant to have two divs, one with the small inlined image (behind) and one with the large image (in front). The large image has
opacity: 0
, when the large image has loaded it transitions toopacity: 1
. The effect would be similar (perhaps not quite as pretty) but I imagine the simpler operation would perform much better on low-range devices. It would also work on IE and older Android browsers (unlikefilter()
).LOVE this concept and might use it in my studio’s portfolio site, as well as one or more client sites that have large background images.
However, I just want to point out that at least in my environment (on a 1.7 Ghz rMBP and a 10 Mbps connection), I couldn’t get the demo to work well in any browser even when force-loading without the cache. On Safari, it jumps straight to the full-res image; on FF and Chrome, it shows the blurred version initially but then jumps straight to the full-res images without transition. 120k seems comically small for a large background image this in day and age. For the sake of ensuring that the demo can work for people on decent broadband connections, you might consider bumping up to something at least 500k.
On the subject of better browser support: It’s not quite as smooth but couldn’t you create a second div above the real header that the blurry background sits in? Then once you lazy-load the real image and have it in place, use
opacity
to fade out the blurry image? It would be more ideal to be able to transition the blur off instead of transitioning straight to a non-blurry image but this solution allows the transitioning to work on a much wider variety of browsers..I hated the thing on Medium. Its the only irritating thing in the Medium.
Now I’ll be seeing more of this in many websites..
Meh!!
This is a great read, Thanks for sharing Emil.
The tiny 40×22 image that you have at ~1KB can be reduced 50% more to ~473B.
You can see it/download it here: http://imgur.com/rke9rgE
And guess what I used to compress it… the best image compression engine ever created: Adobe Fireworks ;)
For anyone running WordPress and wanting to use this technique, we’ve implemented a similar solution to do this automatically as a WordPress plugin here : https://wordpress.org/plugins/featured-image-sharpen-up/
Not sure facebook can take all the credit for this. Google Maps has used this technique a lot since it was built. (More so in satellite images and even more in streetview)
Unfortunately, your SVG solution seems to fail on all versions of IE + Edge, but for reasons I can’t quite understand.
See, the SVG will work as-is if used in an img (src=”data:…”), but when used as a background image, it fails to show unless the background-size is 1280px or less. Why 1280? No idea.
After playing around with it some, it looks like the issue is with the filter. If the filter is removed from the SVG, the background-image will render, but of course without the blur, which is the whole point.
Oh god, I just had a terrible idea. I could always hack together a solution where I position:absolute an image on top of a div to sort-of fake a background image.
Actually, after thinking about my terrible idea for a bit… it’s not so terrible.
One could position the svg image on top of the div, with all the content inside the div positioned relative and with a higher z-index + opacity than the svg
Then, once the large image has loaded, you add that image as a background-image to the div, then lower the opacity of the SVG to 0 with a transition, thereby reversing the blur effect, which would work on all browsers that support SVG filters and Opacity (IE10+?).
Or, I could just… not do any of that and cry in a corner.
Hi Eli, thanks for the feedback. I’m not seeing any of the issues you described on IE or Edge – tested on various versions, screen sizes and OSes (clean virtual machines from modern.ie). Mostly, it works fine, but obviously sans the animation bit.
The closest I get is that the blurred image sometimes doesn’t show before the sharp image loads, but I can’t reliably reproduce that.
My hunch is that the performance of the SVG filter on larger viewport sizes is poor on IE, so that it doesn’t have time to render the blur before the sharp image loads. Maybe. This is a proof-of-concept technique (in my mind, at least), so milage may most definitely vary across browsers… :-)
It’s not a performance thing. The SVG won’t render as a background if it’s more than 1280px at any time.
Try this pen, modified so the JS never actually adds the big image.
The SVG never renders on IE+Edge, but works fine everywhere else.
Interesting – I tested again just now with no JS in IE10/Win7, IE10/Win8, IE11/Win7, Edge 12/Win10 and Edge13/Win10 now – all of the IEs work absolutely fine (i.e. display the blurred SVG on any screen size) on my machine, but I could replicate the 1280-pixel-bug in both the Edge versions.
Sounds like a juicy bug for the Edge team, I’ll send them a report. :-)
It could be a windows 10 thing. Upon further testing, ie11 in win 7 is fine, but ie11 in windows 10 has the 1280 problem.
(Excuse the double post. I accidentally made a new reply below, but you might not get a notification from it)
It could be a windows 10 thing. Upon further testing, ie11 in win 7 is fine, but ie11 in windows 10 has the 1280 problem.
Reader Ryvan Prabhu writes in:
https://github.com/ryvan-js/bleach
Wout Mertens writes in:
https://codepen.io/wmertens/pen/EXNYQK