Non-bouncy layouts

Dear Lazyweb, what's the done thing for making your text not bounce around in dynamic layouts?

I want my images to be dynamically sized to a percentage of the width of the window, not have hardcoded pixel sizes. And yet, I do not want this to force me into a 1994-style reflowing layout where the text moves around as the images load.

In other words, if the final layout looks like this:

All work and no play
img
makes Jack a dull boy

then before the image loads, I want it to look

like this:
All work and no play

makes Jack a dull boy
instead of like this:
All work and no play
makes Jack a dull boy

In the Bad Old Days, you fixed the bouncy-layout problem by specifying the image's width and height in pixels, but that doesn't work if you want the image to scale. In This Modern World, you have to specify "width:100%; height:auto" to accomplish that, and the "auto" means that the image shows up as zero-height until it has begun loading and its native size (and thus its aspect ratio) is known.

This sucks.

Basically I want some way to tell layout, "the intrinsic size of this image you're about to load is 1000px × 400px", or "the intrinsic aspect ratio of this image you're about to load is 10:4".

Is there any sane way to accomplish this? I guess it could be done in JavaScript, but I think you might end up needing to walk up the tree and basically duplicate the entire layout engine to make it work...

Previously.


Update:

Wow. Well, based on the comments here, and based on what I see out in the world, I think the answer is:

No. There's no sane way to fix it.

Every web site must choose between hardcoded image sizes; and having the text thrash around as the images load.

It's 1994 all over again.

Tags: , ,

52 Responses:

  1. Mark Beeson says:

    Try this: create a div element, width: 100%, height: (an initial height you'd like to see), background-url: your image, and background-size: cover (or contain if you don't mind some cropping).

    Then, in Javascript, listen for onDOMready, get the true width of the div, and change the height based on the ratio you're looking for.

    This might give you a weird flash of the image scaling, but it may be a better experience than the empty-image-reflow that you're getting now.

    • jwz says:

      What you have just said is equivalent to: specify <img style="width:100%; height: height-you'd-like-to-see">

      That doesn't help at all.

      • Mark Beeson says:

        The background-size: cover will fix the distortion that you get from the percentage width and fixed height. The image should still show up with a correct ratio, just smaller than you'd like prior to ondomready.

        • jwz says:

          That sounds like it would look ridiculous. Show me an example.

          • So I wrote a quick working example.

            Two comments:

            1. I cheated and used jQuery, but the script in question could pretty easily be ported to native JS code.

            2. On Chrome and my network connection, I couldn't actually get the pre-resize version of the page to ever show up. I had to add a setTimeout() to the function call in order to try to simulate what the image would look like pre-resize.

            This starts out at 400px (or, again, whatever you'd like) and resizes to the correct aspect ratio. This doesn't completely solve the problem, but it alleviates the symptoms, and imho is a lot easier to work with than, say, an inline SVG thing. As a bonus, the tinfoil hats who have Javascript turned off still get the full image.

      • Seems like a pretty clever solution to me, actually. Maybe you missed the part where JS is fixing the height once the page is laid out.

  2. Jesse Mullan says:

    You could set the img src to be a base64 encoded blank png that has the correct ratio, set the width to a percentage and the height to auto, supply a data attribute data-fullsrc="somefile.ext", and then have a bit of javascript that preloads any data-fullsrc attributes and then swaps out the image's src when the loading is complete. A blank png should compress to a very short string, and since it's inline, the blank should render instantly, meaning that there won't be any reflowing.

    Something like:
    [img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==" data-fullsrc="http://upload.wikimedia.org/wikipedia/commons/3/31/Red-dot-5px.png" alt="red dot" /]

    Except that the data would be blank (or a thumbnail url with the same ratio) instead of exactly the same as the full sized-image.

    See also Data URI scheme

    • Max says:

      I can see this working. If you're not hip and HTML5 yet, you could probably even stash that in a lowsrc attribute and use the regular src attribute for your proper image.

    • Ben Morrow says:

      This works, at least in Firefox, though you may not consider it sane:


      img {
      width: 100%;
      height: auto;
      }
      img.real {
      position: absolute;
      top: 0px;
      left: 0px;
      }

      <div style="width: 60%; height: auto; position: relative">
      <img src="data:image/gif;base64,R0lGODdhBgAEAIAAAP///wAAACwAAAAABgAEAAACBISPqVcAOw">
      <img class=real src="/real/photo">
      </div>

      The base64 is a gif with the appropriate aspect ratio. (Gif because it comes out smaller than png. I tried data:image/x-pbm,P1%0a10%204 but unfortunately FF didn't want to play.)

      • Richard says:

        Ah, no, doesn't work for me in Firefox (13), nor in Safari (5.1) for that matter.
        The "real" image floats above any following layout, the div's browser-computed height never appears to change from the inline hack image's pixel height.
        Sounded like an ideal, though.

        • Ben Morrow says:

          Works for me in FF 13. Are you sure you applied the width: 100%; height: auto; to both images? You are correct that the layout doesn't reflow when the second image loads: that's the point. This means the aspect ratio of the inline image needs to be strictly correct.

          • Richard says:

            I don't have my example of the image floating over following text any more, but here it is not working in the opposite fashion. I didn't try to "apply any attribute" except by trying to copy and paste your code and see if it worked.

            Here's the full, concrete, self-contained example I am using, which has problems (assumes 1:1 aspect ratio of "real" img because that is the aspect of the placeholder, leading to excess bottom pad in this case) when I try in FF 12 or Safari 5.1.

            This is the limit of the CSS I wish to be involved with the the rest of the year. Over to others.


            <head>

            <style type="text/css">

              img { width: 100%; height: auto; }

              img.real { position: absolute; top: 0px; left: 0px; }
            </style>
            </head>

            <body>

            <p>Some text</p>

            <div style="width: 60%; height: auto; position: relative">
              <img src="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==">
              <img class=real src="https://cdn.jwz.org/images/dna-sketchup.jpg">
            </div>

            <p>And more 01</p><p>And more 02</p><p>And more 03</p><p>And more 04</p>
            <p>And more 05</p><p>And more 06</p><p>And more 07</p><p>And more 08</p>
            <p>And more 09</p><p>And more 10</p><p>And more 11</p><p>And more 12</p>
            <p>And more 13</p><p>And more 14</p><p>And more 15</p><p>And more 16</p>
            <p>And more 17</p><p>And more 18</p><p>And more 19</p><p>And more 20</p>
            <p>And more 21</p><p>And more 22</p><p>And more 23</p><p>And more 24</p>

            </body>

  3. Bellfry says:

    The only way I was able to make this work is by creating an SVG image of the same aspect ratio (but smaller values, i.e. the SVG image has to be always scaled UP in both dimensions, never down), reference the desired JPG/PNG from there, and then base64-encode this and put it into a data: URI. Advantages: doesn't require JS. Disadvantages: Images don't load progressively (there might be some hackish workarounds like using thumbnails in a layer below full images); will probably only work in newer browsers. I've only tested it in Opera 11.62.

    An example is under: http://pastebin.com/F00VtYm7 - It references a 2.5MB image with a ratio of 100:82 from the wikimedia servers, so some patience is required. For even more graceful degradation one might consider fiddling around with an OBJECT element referencing a standard IMG element for fallback.

    • Bellfry says:

      Disregard what I said about the SVG image having to use smaller values. It also works if they are identical to those of the final JPG/PNG image.

    • Bellfry says:

      Okay, turns out you don't even need base64, plain text works as well if you use single-quotes and escape the less-than character. So this:

      <img src="http://url.to/img.jpg" />

      "simply" turns into this:


      <img src="data:image/svg+xml;charset=UTF-8,&lt;?xml version='1.0' encoding='UTF-8' standalone='no'?>&lt;svg xmlns:svg='http://www.w3.org/2000/svg' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' width='${WIDTH}' height='${HEIGHT}' version='1.1'>&lt;image x='0' y='0' width='${WIDTH}px' height='${HEIGHT}px' xlink:href='http://url.to/img.jpg' />&lt;/svg>" />

      As said before, only tested in Opera 11.62.

      • jwz says:

        That's, uh... clever?

        It's impossible for me to believe that it's portable, though. Or that it will work in any browser more than 5 minutes old. SVG, geez...

        • Bellfry says:

          It's more portable than you probably think. My previous reply with statistics didn't make it through the filter, but the gist was that SVG-in-IMG support is already at 75% and data: URI support is at 97%. The rest can be handled with fallback error handlers on the IMG elements like this:


          onerror="javascript:(function (n) {n.setAttribute('onerror', '');n.setAttribute('src', 'http://url.to/img.jpg'); })(this);"

  4. James Hartig says:

    You can use CSS3 CSS properties. http://www.netmagazine.com/tutorials/control-image-aspect-ratios-css3

    Although, you should probably set the width and height to percentages on a div and then put the image inside of it maybe as the background image and it would scale.

  5. Cheat and use lowsrc before they decide to stop supporting it.

    They.... probably stopped using lowsrc a while ago, didn't they?

    Otherwise you could use a spacer gif to "pre stretch" these windows?

    • chad says:

      Did you really just say `spacer gif'?

      • hattifattener says:

        Considering that the front-running alternative seems to be to hardcode a bunch of hand-adjusted layouts for whatever devices and browsers are popular this week, "spacer gifs" seem like the one of the least terrible things in this thread. Heck, I'd even accept spacer .xpms.

      • yes i did. problem?

  6. urdh says:

    Something like this JSFiddle might work, but needs some work to be responsive when resizing the window (i.e. changing the image width).

  7. Joe Crawford says:

    There's a lot of hoo-hah about responsive images but most involve new HTML tags. If past reading of your thoughts on HTML is any indication, that's not up your alley.

    A more pragmatic approach might be to avoid adding new markup, and instead add some new CSS to mitigate the effect. Now, this solution presumes an aspect ratio of roughly 10:4 as you indicated (with 20% lopped off for the width of the browser window) - but the way this works is by letting CSS know that there's a min-width for those images. Like this:


    /* Desktopish */
    @media screen and (max-width: 1280px) {
    .layout img {
    min-height: 400px;
    }
    }

    /* iPadish */
    @media screen and (max-width: 1024px) {
    .layout img {
    min-height: 323px;
    }
    }

    /* iPad (portrait) */
    @media screen and (max-width: 768px) {
    .layout img {
    min-height: 242px;
    }
    }

    /* iPhone */
    @media screen and (max-width: 480px) {
    .layout img {
    min-height: 151px;
    }

    }

    /* iPhone (portrait) */
    @media screen and (max-width: 320px) {
    .layout img {
    min-height: 101px;
    }
    }

    This does presume the aspect ratio is going to be the same. If your images fall into rough categories - say landscape, portrait, and square just have .layout img.landscape, .layout img.portrait, .layout img.square and specify min-width for each of the screen resolutions you care about.

    I have not delved into the code, but Twitter Bootstrap CSS framework sort of does what you're asking. But it requires you to put class "thumbnail" on everything, enclose that in an unordered list with class "thumbnails" and then enclose /that/ in spanX where X is the number of predefined columns you want the images to span. If you implemented a fluid layout Twitter Bootstrap design and did that, I think you get what you want, but you'd have to embrace all of Twitter Bootstrap for that. Still the code is open and might provide inspiration to some other LazyWebber to provide an elegant solution to your very simple yet somehow complicated need.

    • jwz says:

      That's no good because if you specify a min-height on an image, the image distorts. For this to be correct there would have to be a @media selector for every possible pixel width of the window.

      • Joe Crawford says:

        That will be true if min-height is set to exceed the actual possible "organic" heights. The loaded image will override that minimum. If there's a wild range of possible images, this falls over, true.

        Your use case keeps getting discussed: Responsive Images: How they Almost Worked and What We Need for discussion of how far we have yet to go with getting this right.

      • Nick Thompson says:

        The best approach I've seen is in Twitter Bootstrap 2.0. You have a @media selector for the "pixel" widths that occur in the wild - iPad and iPhone in standard orientations, 240px wide for some tiny screens and some examples for larger sized desktops. Then you add whitespace in the layout that allows it to expand smoothly to in-between sizes, and hopefully it doesn't start to look too bad before you reach the next hand-tweaked size. At larger monitor sizes people expect a lot of whitespace anyway and you can just pad the left and right margins.

      • Going a bit off topic, I have a Java Clock that loads up an image in the background. Yes, I know Java isn't the same thing as JavaScript, however, I was able to get the image to load up for any size possible.

  8. So, tis is terrible, but you could use some JavaScript to grab the window's width and send it to the server, then redirect to the "real" page. On the server-side, stach the width in the session and then set the width and height in pixels. Then, on the client sode, have an onLoad that sets the width to a percentage, and the height to auto (should result in no size change), and an onResize that sends the new width to the server.

    Horrible, but portable...

  9. Grey Hodge says:

    As per your update, yes, there is no sane way. I was going to leave a comment earlier stating this, but it was pretty clear from the comments that was the case. No one in browser development cares about how pages look during loading, and the standards groups are resistant to ideas that a majority of members don't like, regardless of usefulness. I'm friends with someone on the CSSWG and he's expressed his frustration with the process too. "Yes, it's an idea that lots of people would find useful, but we don't like it, so no."

  10. Edouard says:

    Honestly, one of my (many) problems with the web is that the text layout seems less capable than the copy of Quark Xpress 3.3 I used in 1995, and that ran on a machine with 4M of RAM.

    I'm usually all "call me in a decade when this shit works, and HTML, CSS, the server side, the database, and everything else have replaced by a nicer Javascript of the future".

    But I'm pretty sure I started saying that over a decade ago, so I'm pretty sure it will suck just as bad in 10 years time.

    Hmmm. That's not exactly positive. Sorry.

  11. Barry Bethal says:

    If you know the aspect ratio of your image you can achieve this using padding on the container. Say your image is 16/9:

    .container {position:relative; padding-bottom: 56.25% }
    .container IMG {width: 100%; height: 100%; position: absolute }

  12. Matt Neary says:

    A child element's margin will be sized proportionally to to the width of the parent. So here's a working example:

    #container {
    display: inline-block;
    position: relative;
    width: 100px;
    }
    #container .filler {
    margin-top: 75%;
    }
    #container img {
    position: absolute;
    top: 0;
    bottom: 0;
    width: 100%; height: 100%;
    background: silver;
    }

    • jwz says:

      So, I was aware of this trick because it's how I get embedded videos to scale properly (which is a bigger pain in the ass than images, since iframes and embeds have no intrinsic size at all). See the classes video_floater, video_frame and video_embed on any page with video on this blog or the DNA site.

      But there, I make the assumption that all videos are 16:9, which is fair.

      To do this for images, you'd need those 3 classes (or long style arguments) wrapped around every single image. Which is, like, kind of a pain in the ass and pretty verbose.

      Does any site out in the real world actually do this?

      Also, something I learned from doing the video aspect stuff is that just about every HTML "sanitizer" out there (mail clients, RSS readers, including Gmail and Google Reader, etc. etc.) strip all the margins out of your inlined style= options. So you have to emit completely different HTML for those, too.

      Good times. Good times.

      • Matt Neary says:

        It could be simplified with a pseudo element instead of the filler class leaving you with just a container which is pretty common. I don't know if that changes anything for you, but it would at least clean up the markup.

  13. Eric Demicco says:

    I think it's possible. You could use data attributes on the img tag: .

    Then your JavaScript uses those as well as the measured width to figure out what the height should be and sets it. This should be supported pretty widely (old IE, even).

    • Eric Demicco says:

      Sorry, that would be <img src="images/blah.jpg" data-nativewidth="1024" data-nativeheight="768">

    • Eric Demicco says:

      I think I got it. You still see the zero-height images for a split-second before the JS runs but I don't think that's going to get any better. I don't think it's too insane.

      Proof-of-concept here:
      http://ericdemicco.com/non-bouncy/


      function fixBouncy() {
      var images = $('img');

      images.each(setHeight);

      function setHeight(i,v) {
      var image = $(v);
      var actualWidth = image.width();
      var nativeHeight = image.data('nativeheight');
      var nativeWidth = image.data('nativewidth');
      var imageAspect = nativeWidth / nativeHeight;

      image.height(actualWidth * (1 / imageAspect) );
      }
      }

      $(document).ready(fixBouncy);

      $(window).resize(fixBouncy);

  14. Shiv Menon says:

    Use something like jpegmeta.js to parse the headers, ie, get the width and height and then render it on to the page using client side templates.

    • jwz says:

      So, instead of loading the image via HTML, load it via JavaScript, then load it via HTML again. Yeah, that sounds faster. If time runs backwards.

  15. Sam Rossoff says:

    I have probably misunderstanding what you want, because it sounds like you want:

    <img id="correctsizedimage" style="width:100%; height:auto" src="http://upload.wikimedia.org/wikipedia/commons/3/31/Red-dot-5px.png" />
    <script>
    var temp = document.getElementById('correctsizedimage');
    temp.style.height = temp.width*4/10; //or whatever aspect ratio you wanted
    </script>

    Or maybe there are browsers that doesn't work on?

    The issue of time:
    Chrome says this takes 0 ms, but I'm guessing their benchmarking isn't fine grained enough. I tried benchmarking with new Date(), but that only has millisecond resolution, and there is no guarantee that it'll include rendering time (it also came up with 0)

  16. Dan L says:

    Your static html page can be the output of tool that parses the headers, reads an input page template, then writes the HTML with the correct image dimensions.

    • Stuhacking says:

      That's not reallly a static html page anymore, though. You just invented server side rendering.

      • Dan L says:

        Just treat templates and images as source code and the html as object code. Not really server side rendering because the server still serves static pages; they just aren't hand coded (not directly at least.)

        How is the client to know the dimensions of the image unless the server specifies it ahead of time? Seems like any other approach is going to be awkward and hacky.

  17. hwc says:

    sounds like something that needs to be added to the next standard.