53

I'm trying to create an iOS 7 style frosted look with HTML5, CSS3 and JavaScript which can work on webkit browsers.

Technically, given the following HTML:

<style>
  #partial-overlay {
    width: 100%;
    height: 20px;
    background: rgba(255, 255, 255, .2); /* TODO frost */
    position: fixed;
    top: 0;
    left: 0;
  }
</style>
<div id="main-view">
  <div style="width: 50px; height: 50px; background: #f00"></div>
  To my left is a red box<br>
  Now there is just text<br>
  Text that goes on for a few pixels <br>
  or even more
</div>
<div id="partial-overlay">
  Here is some content
</div>

I'd like to apply something like a -webkit-filter: blur(5px) to the first 20px horizontally of #main-view.

If the CSS was modified to be #partial-overlay { width: 20px; height: 100%; ...} then I'd need to apply the -webkit-filter: blur(5px) to the first 20px vertically.

The obvious solution is to use javascript to make a clone of the #main-view, set overflow: hidden and then change the width/height as appropriate but that seems to me hard to generalize to more complex pages/CSS structures.

Is there a better way to achieve this with minimal performance hit and maximal generalizability?

EDIT: Here is an example of what I'm trying to achieve: Mockup

Aaron Yodaiken
  • 19,163
  • 32
  • 103
  • 184
  • CSS is an all or nothing thing. You can't just apply a property to an arbitrary portion of it. The closest you can get to "only the first 20px" is by using the `:first-line` pseudo element. – cimmanon Jun 13 '13 at 16:37
  • @cimmanon see edited "obvious solution" – Aaron Yodaiken Jun 13 '13 at 17:25
  • Could you show an image example of what you're looking to do? If I read the issue correctly you could try using the multiple backgrounds ability of css to achieve this. EDIT: also, here's a JSFIDDLE of his example for anyone who's answering this and want to test: http://jsfiddle.net/c6Lwf/ – aaron-bond Jun 14 '13 at 15:21
  • @cimmanon, just tried and `webkit-filter: blur` doesn't seem to work with `first-line`, looks like [only some styles](http://www.w3schools.com/cssref/sel_firstline.asp) are allowed for that pseudo class – Subhas Jun 15 '13 at 19:40
  • Might want to look at these sites: http://davidwalsh.name/demo/css-filters.php & http://css-tricks.com/fun-with-blurred-text/ – Dom Jun 15 '13 at 20:00
  • I was looking at the Apple homepage and checking on how they created this partial blur effect. It turns out that they used images for this. I guess there is no simple way of doing this and your suggestion in the question is the only viable way to achieve this effect. It is possible to generalize it by making calculations from the overlay div but there will definitely be a performance hit. – Antony Jun 16 '13 at 04:38
  • aaron, I changed mine, it now looks alot more like the image you showed – Connor Jun 16 '13 at 05:38
  • Doesn't `box-shadow` help ? – Jashwant Jun 17 '13 at 04:29

10 Answers10

21

Thanks for the inspiration... It led me to this canvas plugin which does the trick

New and Improved: -webkit- and Firefox Working Example, now re-sizable/fluid.

JS

$(document).ready(function () {
    frost = function () {
        var w = $('#main-view').width();
        html2canvas(document.body, {
            onrendered: function (canvas) {
                document.body.appendChild(canvas);
                $('canvas').wrap('<div id="contain" />');
            },
            width: w,
            height: 30
        });
        $('canvas, #partial-overlay, #cover').hide();
        $('#cover').fadeIn('slow', function () {
            $('#partial-overlay').fadeIn('slow');
        });
    };

    $('body').append('<div id="cover"></div><svg id="svg-image-blur"><filter id="blur-effect-1"><feGaussianBlur stdDeviation="2"/></filter></svg>');

    $('#main-view').click(function () {
        frost();
        $('#partial-overlay').addClass('vis');
        $(window).resize(function () {
            $('canvas, #partial-overlay, #cover').hide();
        });

        function onResize() {
            if ($('#partial-overlay').hasClass('vis')) {
                frost();
            }
        }
        var timer;
        $(window).bind('resize', function () {
            timer && clearTimeout(timer);
            timer = setTimeout(onResize, 50);
        });

    });

    $('#partial-overlay').click(function () {
        $('#partial-overlay').removeClass('vis');
        $('canvas, #partial-overlay, #cover').hide();
    });
});

CSS

#main-view {
    width:75%;
    height:50%;
    box-sizing: border-box;
    margin:8px;
}
#partial-overlay {
    display:none;
    width: 100%;
    height: 20px;
    position: absolute;
    top: 0;
    left: 0;
    z-index:99;
    background: rgba(255, 255, 255, 0.2);
    cursor:pointer;
}
canvas {
    position: absolute;
    top: 0;
    left: 0;
    -webkit-filter:blur(5px);
    filter: url(#blur-effect-1);
}
#cover {
    display:none;
    height:19px;
    width:100%;
    background:#fff;
    top:0;
    left:0;
    position:absolute;
}
#contain {
    height:20px;
    width:100%;
    overflow:hidden;
    position:absolute;
    top:0;
    left:0;
}
svg {
    height:0;
    width:0;
}

HTML

<div id="main-view">
    <div style="width: 10%; height: 20%; background: #f00; float: left"></div>To my left is a red box
    <br>Now there is just text
    <br>Text that goes on for a few pixels
    <br>or even more</div>
<div id="partial-overlay">Here is some content</div>

I put it in a click function, because I figured it would be the most likely use case. It will work just as well on document ready.

Although the canvas representation wont be pixel perfect, I don't think it will really matter in most cases because its being blurred.

Update: As requested this is now re-sizable. I also moved the cover div into the JS and added an svg fall back for Firefox. The resizing requires the canvas to be redrawn on each re-size, so I set it up to hide the canvas, overlay, etc while you're resizing and then replace it when the re-size stops.

apaul
  • 16,092
  • 8
  • 47
  • 82
  • If you could somehow make an example where the background image is made by one of those "javascript screenshot" things and then blurred, that might be a really smart answer. – Aaron Yodaiken Jun 16 '13 at 16:15
  • This is just about the trick. The only problem is that the blur fades out at the edges of the canvas (not really a problem around the edges, but definitely at the bottom). Perhaps this can be fixed with some wrapper divs and whatnot, but I couldn't seem to get it to work quickly. – Aaron Yodaiken Jun 16 '13 at 21:14
  • It looks decent, but it's still not blurring the same as if the canvas was cut off at 20px or whatever which is what I'm ultimately aiming for. – Aaron Yodaiken Jun 16 '13 at 23:04
  • There's something funny going on with text in the blur (it's not fully blurring or maybe the canvas is really not transparent or something like that). See http://jsfiddle.net/TfWs3/33/ – Aaron Yodaiken Jun 17 '13 at 02:26
  • @AaronYodaiken if you need it to be a little more cross browser compatible check this out, you can use svg as a fall back for firefox http://jsfiddle.net/apaul34208/TfWs3/61/ – apaul Jun 18 '13 at 03:10
  • If you could make a contained JS function that does everything with fluid width and whatnot (like Ken did) I'll give you the bounty for you hard work over this. Thanks! – Aaron Yodaiken Jun 18 '13 at 03:10
  • [There is a similar question](http://stackoverflow.com/q/31303856/4639281), but I don't feel like digging into this to make it work for that question. Could you offer some input there? –  Jul 09 '15 at 00:37
13

Basically you could have a overlay placeholder where you duplicate the main content and sync the scrolling of both divs, than apply css blur filter on the overlay only.

A simple javascript will do:

$(document).ready(function(){
  $('.overlay').append( $('.content').html() );
  $('.content').on('scroll', function(){
    $('.overlay').scrollTop($(this).scrollTop());
  });
});

And for the CSS:

.overlay {
    overflow:hidden;
    -webkit-filter: blur(.25em);
    background:#fff;
}

I put together a working example for you (webkit only):

http://jsfiddle.net/kmxD3/1/

Have fun! :)

Gerson Goulart
  • 584
  • 6
  • 14
  • 3
    This seems to be by far the simplest and most elegant solution I've come across. – mredig Jan 25 '15 at 22:38
  • This approach is pretty awesome, however I can see some uneven frosting on the top-part of the div. Any idea what might cause this? – Automatico Feb 08 '15 at 10:36
  • 2
    @Cort3z, this is due to the fading out of the blur effect (which allows you to see the regular text underneath. To better understand, look at the images in the blur example in this post: http://www.html5rocks.com/en/tutorials/filters/understanding-css/ (note the edges of the image in the right fading to white). To account for that and have a sharp edge with blurred content you'd have to make the blurred content a little bigger and then clip it (or make it go off-canvas) for a sharper edge). I hope I made it clearer... This example should have a sharper edge: http://jsfiddle.net/kmxD3/105/ – Gerson Goulart Apr 15 '15 at 23:15
11

Is there a better way to achieve this with minimal performance hit and maximal generalizability?

The answer to this is no.

The reason is that in order to do what you want you would need direct access to the bitmap used for the browser window to extract or manipulate the pixels in the area you want to blur (I wish, "aero" in a browser could look pretty neat..) or a filter that works on the elements behind the one you apply it to (or can have a limiting region set to it).

As there is no native support to do this (besides canvas and extension API, or a library that generate canvas image from the html -> relatively slow) this will need to be done with trickery (images, splitting divs etc.) in either case.

If you made everything in your page on a canvas you could do a lot of interesting things, but you would also need to perform layout, update, filtering etc. yourselves and therefor you would be back no non-optimized as Javascript is slower than native (not to mention it would be error prone).

Until browsers allow you to grab a section of the window as a canvas (never? as that would require everything on that page to be same-origin or have content with special accept headers set) there is no way around but to do tricks.

Update

As an demonstration that you can do it by using html2canvas etc, but having to use compromises (-> slow performance) - the demo is rough, experimental and needs tweaks to work well - but for the sake of demo only:
http://jsfiddle.net/AbdiasSoftware/RCaLR/

Result:

enter image description here

Generalized function to grab part of background:

getBlurredBG(x, y, width, height, id);

Get part of window using html2canvas:

function getBlurredBG(x, y, w, h, id) {

    html2canvas(document.body, {
        onrendered: function (canvas) {
            process(canvas, x, y, w, h, id);
        },
        width: (x + w),
        height: (y + h)
    });
}

Process the content:

function process(canvas, x, y, w, h, id) {

    //create new canvas to enable clipping
    //As html2canvas returns a canvas from 0,0 to width and height
    //we need to clip it.
    var ocanvas = document.createElement('canvas');
    ocanvas.width = w;
    ocanvas.height = h;
    ocanvas.style.left = x + 'px';
    ocanvas.style.top = y + 'px';
    ocanvas.style.position = 'absolute';
    ocanvas.id = id;

    var ctx = ocanvas.getContext('2d');
    ctx.drawImage(canvas, x, y, w, h,
                          0, 0, w, h);

    stackBlurCanvasRGB(ocanvas, x, y, w, h, 28)
    lighten(ocanvas);

    ctx.fillStyle = 'rgba(255,255,255,0.5)';
    ctx.fillRect(x, y, w, h);

    ctx.fillStyle = '#999';
    ctx.font = '32px arial';
    ctx.fillText("Partial overlay content", 10, 60);

    document.body.appendChild(ocanvas);
}
  • 1
    I know that I've seen things that take screenshots of the whole page and you can send as PNG or whatever (like Google+ used to have). Isn't that window -> canvas? – Aaron Yodaiken Jun 16 '13 at 15:53
  • @AaronYodaiken that's canvas. Libraries such as html2canvas can do this. It's however not an actual snapshot, but a close representation based on the element's geometry (element->boundaries->type->render close approx.). And for none-origin images it needs to use a proxy. But works for the purpose. –  Jun 16 '13 at 20:10
  • @AaronYodaiken I do mention this in my answer, but it would be slow for "real-time" use (perhaps an optimized partial "snapshot" could work well enough). One can always cache the snapshot but the user would need to wait while that happen. There will always be a need for a compromise as there is no native support. –  Jun 16 '13 at 20:13
  • Thanks very much for the thoughts and this solution. If I could split the bounty I would. – Aaron Yodaiken Jun 18 '13 at 17:33
10

Recently, a new -webkit-backdrop-filter. This is currently supported in Safari 9.0, and Chrome behind a flag.

Demo:

#blurred {
  -webkit-backdrop-filter: blur(10px);
  width: 200px;
  height: 100px;
  position: fixed;
  top: 50px;
  left: 50px;
  border: 3px solid white;
}
<img src="https://upload.wikimedia.org/wikipedia/commons/e/eb/Ash_Tree_-_geograph.org.uk_-_590710.jpg">

<div id="blurred"> Blurred </div>

Support right now is only Safari. Chrome and Opera have this under a flag.

Note: Today -webkit-backdrop-filter still doesn't have great support so for now if you want to get this effect, the best way is using SVG's feGaussianBlur

Downgoat
  • 13,771
  • 5
  • 46
  • 69
  • This is the official solution. Can set this as the answer? – trusktr Dec 13 '15 at 06:53
  • @trusktr No, because it's only available in Safari, which is not really popular compared to Chrome and Firefox. – Luca Steeb Jan 10 '16 at 13:47
  • @LucaSteeb Chrome supports it now, behind a flag. Hopefully this becomes the answer in the near future, as backdrop-filter will be the actual way to do it. The other answers are just cheap hacks. – trusktr Jan 18 '16 at 22:47
  • @trusktr Cheap hacks is maybe a bit exaggerated, but generally you're right. The problem is that even when all new browsers support it, what are you doing with users which are using old browsers - it's probably not a small amount.. – Luca Steeb Feb 14 '16 at 23:45
  • @LucaSteeb Indeed, that's a problem, but doesn't change the fact that the other methods are still just hacks. Native has had this ability forever. It's finally time for the Web to start getting real features. – trusktr Feb 22 '16 at 21:26
6

CSS only solution: backdrop-filter: blur(4px);

Björn Hjorth
  • 2,459
  • 5
  • 25
  • 31
4

I've made some css-trick without backdrop-filter because chrome doesn't support it by default

My original code with sass: https://codepen.io/mixal_bl4/pen/EwPMWo

$(()=>{
  let sdm = $('.some-draggable-modal');
  sdm.center().draggable();
});

jQuery.fn.center = function () {
    this.css("position","absolute");
    this.css("top", Math.max(0, (($(window).height() - $(this).outerHeight()) / 2) + 
                                                $(window).scrollTop()) + "px");
    this.css("left", Math.max(0, (($(window).width() - $(this).outerWidth()) / 2) + 
                                                $(window).scrollLeft()) + "px");
    return this;
};
body {
  background: -webkit-repeating-linear-gradient(135deg, #fff, #fff 25px, #e2edc1 25px, #e2edc1 50px) fixed;
  background: repeating-linear-gradient(-45deg, #fff, #fff 25px, #e2edc1 25px, #e2edc1 50px) fixed;
  background-attachment: fixed;
  background-size: cover;
}
html:before,
body:before {
  content: "";
  display: block;
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: rgba(0, 0, 0, 0.3);
}
html .some-draggable-modal,
body .some-draggable-modal {
  width: 150px;
  height: 150px;
  border: 1px solid lime;
  display: -webkit-box;
  display: -ms-flexbox;
  display: flex;
  place-content: center;
  -webkit-box-orient: vertical;
  -webkit-box-direction: normal;
      -ms-flex-direction: column;
          flex-direction: column;
  text-align: center;
  border-radius: 6px;
  cursor: move;
  position: relative;
  overflow: hidden;
}
html .some-draggable-modal .title,
body .some-draggable-modal .title {
  position: relative;
  z-index: 1;
  color: black;
}
html .some-draggable-modal:before,
body .some-draggable-modal:before {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: -webkit-repeating-linear-gradient(135deg, #fff, #fff 25px, #e2edc1 25px, #e2edc1 50px) fixed;
  background: repeating-linear-gradient(-45deg, #fff, #fff 25px, #e2edc1 25px, #e2edc1 50px) fixed;
  background-attachment: fixed;
  background-size: cover;
}
html .some-draggable-modal:hover:before,
body .some-draggable-modal:hover:before {
  -webkit-filter: blur(2px);
          filter: blur(2px);
}
html .some-draggable-modal:hover:after,
body .some-draggable-modal:hover:after {
  content: "filter: blur(2px)";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 10px;
  color: green;
}
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script>

<div class="some-draggable-modal">
  <span class="title">You can drag me :)</span>
</div>
mixalbl4
  • 3,507
  • 1
  • 30
  • 44
3

http://thiv.net/frost

Live sample of what I did ( and updated to look just like the image above )

Code:

<style>
  #partial-overlay {
    width: 100%;
    height: 45px;
    background: #ffffff; /* TODO frost */
    -webkit-opacity:0.70;
    -webkit-filter: blur(5px);
    position: absolute;
    top: 20px;
    left: 0px;
    z-index:5;
  }
  #main-view
  {
   position: fixed;
   top: 20px;
   left: 80px;
   z-index:-1;
  }
</style>
<div id="main-view">
  <div style="width: 50px; height: 50px; background: #f00; position:fixed; left:10px; top: 40px; -webkit-filter: blur(2px); "></div>
    <div style="width: 80px; height: 60px; background: #fff; position:fixed; left:0px; top: 66px; -webkit-filter: blur(5px);"></div>
    <div style="width: 50px; height: 30px; background: #f00; position:fixed; left:10px; top: 60px;"></div>
  <p style="font-family:Sans, Arial, Verdana;">
  To my left is a red box<br>
  Now there is just text<br>
  Text that goes on for a few pixels <br>
  or even more
  </p>
</div>
<div id="partial-overlay">

</div>

I made it look a bit prettier than it needs to be, but It works!

Connor
  • 657
  • 1
  • 7
  • 23
2

Unfortunately there is no nice way of doing this, as you figured out you will need a copy of the main div:

<div class="wrapper">
   <div class="overlay"></div>
   <div id="main-copy"></div>
</div>

Overlay will push the wrapper width around while, main-copy will be in the background with blur. Obviously there will be performance issues if the content in main-copy is complex.

David Nguyen
  • 8,368
  • 2
  • 33
  • 49
2

It has still very limited suport (only Firefox) but one way to get it could be this:

Firefox only demo

The CSS is quite simple:

#partial-overlay {
    width:400px; 
    height:100px; 
    background: -moz-element(#main-view);
    top: 0px;
    left: 200px;
    position: absolute;
    opacity: 0.3;
}

and the key is to use as background for partial overlay the main-view element.

This demo uses only opacity because filters are not availables for Firefox.

The element property for the background has been approved by w3c, so it could show sometime in the future in webkit

In the demo partial-overlay has been shift to the right to make more clear what is what

Community
  • 1
  • 1
vals
  • 61,425
  • 11
  • 89
  • 138
  • I've expanded on this to blur the background using an SVG filter: [JS Fiddle](http://jsfiddle.net/y6veJ/2/) - FX only but much quicker than using `html2canvas`. – Keith Sep 20 '13 at 13:21
  • @Keith Thanks ! It is a real pitty that the background: element feature isn't more widely available, it would allow a lot of posibilities – vals Sep 20 '13 at 16:32
  • Thanks vals, as always ;) – Zach Saucier Dec 23 '15 at 21:07
  • @ZachSaucier Hi ! Happy to see you again !. As a strange coincidence, I was in Poland in september (a familiar wedding, nothing related with software :-) – vals Dec 23 '15 at 22:30
2

You can now use the backdrop-filter CSS property.

Latest versions of Chrome, Opera and Edge support it by default, while Safari and Legacy Edge support it with the -webkit- prefix, and Firefox supports the unprefixed version if the layout.css.backdrop-filter.enabled preference is set to true.

So your CSS becomes:

#partial-overlay {
    width: 100%;
    height: 20px;
    background: rgba(255, 255, 255, .2); /* TODO frost */
    position: fixed;
    top: 0;
    left: 0;
    
    backdrop-filter: blur(10px);
    -webkit-backdrop-filter: blur(10px);
}