Heygrady (there's a new blog)

I don't get It.

Responsive Images Without JavaScript

Permalink

The hot topic in web development right now is how to gracefully handle images when building a responsive website. Images that change based on media queries are known as responsive or adaptive images. The W3C is currently trying to sort out competing specifications and ideas from the community. While the debate continues, many clever developers are coming up with JavaScript-based solutions. But there's a way to provide responsive images without using any JavaScript at all.

If you don't like the srcset solution being proposed, or you simply need something that works in today's browsers and doesn't use JavaScript, read below for a potential solution to the responsive image problem.

TL;DR

This article covers a method for using CSS to handle responsive images instead of using JavaScript. There are two use-cases discussed in-depth below. Part 1 covers how to create an responsive image using only CSS. Part 2 covers how to create a proportionally scaled responsive images using only CSS.

Example: See a live example of a CSS-only responsive image.

Example: See a live example of a CSS-only responsive image that scales proportionally.

CSS-Only Responsive Images

Here's a quick preview of the technique. This method for responsive images uses CSS in a <style> tag to make a <span> behave like an image. This method has broad browser support, is accessible and does not require JavaScript.

First is some generic CSS that belongs in a global stylesheet and can be used for all responsive images. The CSS is for making the <span> behave more like a native <img>. The key to this is display: inline-block and background-size: 100%.

final.cssGist page
/* place these styles in your global stylesheet */
.image {
display: -moz-inline-box;
-moz-box-orient: vertical;
display: inline-block;
vertical-align: middle;
*vertical-align: auto;
font: 0/0 serif;
text-shadow: none;
color: transparent;
-webkit-background-size: 100%;
-moz-background-size: 100%;
-o-background-size: 100%;
background-size: 100%;
background-position: 50% 50%;
background-repeat: no-repeat;
}
.image {
*display: inline;
}
.image .inner {
display: block;
height: 0;
}

NOTE: please see below for an explanation of this CSS.

Second is the base HTML for all responsive images. Essentially the HTML below will replace each <img> tag that needs to respond to media queries. Of course the media queries themselves should be placed inside the <style> tag.

final.htmlGist page
<!-- adaptive image -->
<span class="image-scope">
<style scoped>
/* apply background image and dimensions to the span below */
</style>
<span id="road" class="image" role="img" aria-label="A road in Sweden during winter."><span class="inner"></span></span>
</span>

NOTE: The media queries are explained in step 4 below.

Part 1: Responsive Images Using CSS

There are many good articles about handling responsive images and they all have the same goal of supporting the types of media query wizardry that can be accomplished in CSS for swapping out images. While most of those solutions utilize JavaScript to swap the src of an <img>, I'm proposing that using CSS background images is probably good enough and avoids most of the pitfalls of the other techniques. It's fairly easy to make a <span> with a background image behave exactly like a real <img> with some basic CSS, primarily relying on display: inline-block and background-size: 100%.

Of course, moving every in-page image on your site to your global stylesheets is impractical and ill-advised. The images we're talking about are part of the content and have no place in your site's global styles; they're very likely to be managed by a CMS. The solution is to embed the styles in the HTML by placing a <style> tag in the <body> instead of in the global stylesheet.

A recent draft of HTML5 spec would allow for <style> tags in the <body> as long as they carry a scoped attribute (although this currently won't validate). Using a <style> tag this way is only marginally different than the usage of <source> as proposed in the picturefill method and it's certainly less confusing than the srcset proposal. Using CSS carries with it the benefit of a familiar, flexible syntax and broad browser support. This also avoids relying on an emerging and contentious standard.

Start with a Regular Image

To start, we need a regular <img> that we wish was responsive. The example below is just that: a boring old image that doesn't do anything special.

step-0.htmlGist page
<img src="road.jpg" alt="A road in Sweden during winter.">

Example: See a live example even though this isn't responsive and is just a regular image.

Step 1: Use a SPAN

In order to fake an image using CSS, we need to start with a <span>. Because a <span> is inline by default, display: inline-block is supported on that element in IE6 and IE7. There's long been a usable hasLayout hack that makes IE6 and IE7 apply inline-block to any element but — to me at least — a <span> is already closer to the image layout we want before any styles have been applied. For now we'll place the alt text inside the <span> but we'll fix that in the next step.

Below you can see a simple <span> with an .image class ready to be styled.

step-1.htmlGist page
<span class="image">A road in Sweden during winter.</span>

Example: See a live example although there's not much to see yet.

NOTE: You could use a fictional tag like <picture> instead of <span> but that would require a shim for IE6, IE7 and IE8. You could also use <div> instead of <span> without any adverse effects.

Step 2: Add an ARIA Role

ARIA supplies a role="img" for a container that visually represents an image. Applying this will help ARIA-enabled browsers understand what this <span> is used for. ARIA also specifies that the role="img" is not labeled by its contents so we need to move the alt text to the aria-label attribute.

Below you can see the <span> with an ARIA role applied and the alt text moved to an ARIA label.

step-2.htmlGist page
<span class="image" role="img" aria-label="A road in Sweden during winter."></span>

Example: See a live example although there's still not much to see.

Step 3: Generic Styles for All Images

Now that we've got the mark-up for an accessible image, we need to make it behave like an image using some CSS. This CSS is generic to all .image elements and can be placed in your global stylesheets.

First we'll use Compass to generate our generic styles. Although Compass isn't available in a browser, it's a good start for generating the default styles needed for our new .image elements. Compass already has some mixins for cross-browser inline-block, squishing text and vendor-prefixed background-size. This helps immensely because they've already done the research for us to make this work seamlessly across all browsers. (If you're unfamiliar, learn how to get started with Sass and Compass.)

  • display: inline-block makes the <span> behave like an <img>. Inline-block allows the element to follow the document flow like an inline element but it can also have dimension (height and width) like a block element. Here's an article about inline-block.
  • Squishing text is preferred to hiding text in this case because of some issues with text-indent in mobile WebKit.
  • background-size: 100% forces the background image to stretch to the full size of the element so that it behaves like a real image. Read about background-size on MDN.
  • background-position: 50% 50% helps out browsers that don't support background-size by keeping the image centered in the .image.
  • background-repeat: no-repeat keeps the background from repeating.

Below is the example SCSS code that was used to generate the generic CSS needed for this technique. Again, generating styles with Compass is an unnecessary step in this case but it was included as a shortcut explanation for all of the crazy styles required for cross-browser support.

generic.scssGist page
@import "compass/css3/inline-block";
@import "compass/typography/text/replacement";
@import "compass/css3/background-size";
.image {
@include inline-block;
@include squish-text;
@include background-size(100%);
background-position: 50% 50%;
background-repeat: no-repeat;
}

Once compiled, the SCSS code above looks like the following CSS. The generic styles for .image can be shared for all responsive images on the page. That helps make the specific styles for the individual responsive images more compact and readable. The code below is ready to use and should be placed in your site's global stylesheets.

generic.cssGist page
.image {
display: -moz-inline-box;
-moz-box-orient: vertical;
display: inline-block;
vertical-align: middle;
*vertical-align: auto;
font: 0/0 serif;
text-shadow: none;
color: transparent;
-webkit-background-size: 100%;
-moz-background-size: 100%;
-o-background-size: 100%;
background-size: 100%;
background-position: 50% 50%;
background-repeat: no-repeat;
}
.image {
*display: inline;
}

Example: See a live example although there's yet again not much to see.

NOTE: IE6, IE7 and IE8 (and some other legacy browsers) don't support background-size. This only affects those browser's ability to scale the image. It will work just fine for a fixed-dimension image that is shown at its default size. See background-size support on When Can I Use. It's important to note that support for media queries closely matches support for background-size. Basically it's safe to assume that a browser that doesn't support media queries also doesn't support background-size and vice versa. So your fall-back styles in the next step should take that limitation into account.

Step 4: Specific Styles for the Individual Image

Now it's time to apply the background image to our <span>. We don't want to place these styles in the global stylesheet because our image is specific to the content of our page. The rule of thumb is that if you'd normally use an <img> you probably want it to be in-page using this technique. For each image we'll be placing a <style> element just above it. Each image also needs a unique ID to make sure our styles are applied to the correct image. Although placing a <style> element in the body is not technically allowed in HTML, it actually works just fine in all browsers.

For each media query we need to provide a background image and a height and width. The result is very similar to the mark-up for the Picturefill solution proposed for the <picture> tag. Although using a <style> tag is decidedly more verbose — CSS is not known for brevity — it doesn't require the weird duplicative HTML comments <picture> requires for full browser support. Plus, a pure CSS method doesn't require any JavaScript whatsoever.

The example below shows the <style> element just above our <span> with all of the styles for our responsive image. In this example the fall-back is the desktop version but otherwise a mobile first approach is used in the media queries. The media queries below are simple examples and your project will undoubtedly require different break points.

  • The fall-back should probably be the default desktop image.
  • Each media query should supply a background-image as well as the height and width because the browser can't guess the size of a background image the way it can with regular <img> elements.
  • Other necessary styles like display: inline-block and background-size: 100% are handled by the global CSS styles described in step 3 above.
  • The <span> must be given a unique ID to ensure that styles are applied correctly.
step-4.htmlGist page
<style>
/* fall-back styles for browsers without media query support (IE6-8) */
#road {
background-image: url('road.jpg');
height: 681px;
width: 1024px;
}
/* small image for mobile */
@media (min-width: 0px) {
#road {
background-image: url('road-small.jpg');
height: 213px;
width: 320px;
}
}
/* medium image for tablets */
@media (min-width: 640px) {
#road {
background-image: url('road-medium.jpg');
height: 426px;
width: 640px;
}
}
/* large image for desktop */
@media (min-width: 1024px) {
#road {
background-image: url('road.jpg');
height: 681px;
width: 1024px;
}
}
</style>
<span id="road" class="image" role="img" aria-label="A road in Sweden during winter."></span>

Example: See a live example; now we're in business.

NOTE: Although the vast majority of examples on the web recommend adding the smallest, mobile optimized image as the fall-back, I prefer to supply the default desktop image to the browsers that don't support media queries. The only browsers that get the fall-back image are browsers that don't support media queries, and those browsers are almost exclusively IE6, IE7 and IE8. Supplying the mobile image as the fall-back is silly considering that there aren't any mobile browsers with any noticeable marketshare that require the fall-back. The mobile browsers we're targeting all support media queries.

In general, I'm of the opinion that browsers that don't natively support responsive design should simply be served the vanilla desktop experience, since that's almost certainly what those users were expecting anyway.

Step 5: Add in Scoped

To make ourselves feel better about using invalid markup, let's add scoped to our style tag to at least conform to the new HTML5 standard. This won't make our code validate any better than it would have otherwise and support for the scoped attribute is currently non-existent.

The lack of support for scoped means that we'll have to keep using the id in our styles instead of being able to use more generic .image to add the background image and dimensions. That's ok though; we're only adding in the scoped attribute to appear to be writing valid markup and to hopefully prepare for future compatibility and validity. WebKit already has experimental support and other browsers are sure to follow.

Scoped <style> elements need a wrapper element to define, well…the scope. If we use an inline element like a <span> as the wrapper it will automatically shrink-wrap our responsive image and not have any negative effect on our layout. (**UPDATE:** It appears that inline elements are not valid style scopes)

Below you can see the identical markup from step 4 with the addition of the scoped attribute and a new .image-scope element. The .image-scope element doesn't require any additional styles because it is a <span> and is display: inline already.

step-5.htmlGist page
<span class="image-scope">
<style scoped>
/* fall-back styles for browsers without media query support (IE6-8) */
#road {
background-image: url('road.jpg');
height: 681px;
width: 1024px;
}
/* small image for mobile */
@media (min-width: 0px) {
#road {
background-image: url('road-small.jpg');
height: 213px;
width: 320px;
}
}
/* medium image for tablets */
@media (min-width: 640px) {
#road {
background-image: url('road-medium.jpg');
height: 426px;
width: 640px;
}
}
/* large image for desktop */
@media (min-width: 1024px) {
#road {
background-image: url('road.jpg');
height: 681px;
width: 1024px;
}
}
</style>
<span id="road" class="image" role="img" aria-label="A road in Sweden during winter."></span>
</span>

Example: See a live example that looks exactly the same as step 4.

NOTE: Because of the lack of browser support for scoped, this step is completely optional and only serves to make the mark-up slightly more compliant. Even then, using scoped is pretty controversial — there's severe backwards compatibility issues — and you may wish to skip it altogether.

Part 2: Proportionally Scaling Responsive Images

When working with responsive design, it's becoming increasingly popular to use fluid layouts to ensure that the content is fitting the screen properly. When using a normal <img> it's easy to support scalable dimensions by defining only the width of the image with a percentage and letting the browser proportionally scale the image height. Although it requires some ingenuity, the responsive image techniques described above can also be combined with proportional scaling.

Normal Example

Below is an example of how this is handled with a normal image. The <img> will scale proportionally, meaning the width will always match the width of its container. The height will always be the appropriate height. This way the image will never appear stretched or distorted. This trick is useful for fluid layouts where you don't know the exact width of the column.

proportional-step-0.htmlGist page
<!-- This image will scale proportionally to be the full width of the column -->
<img src="road.jpg" alt="A road in Sweden during winter." width="100%">

Example: See a live example even though this isn't responsive and is just a regular image. It does scale proportionally though.

Step 1: Adding an Inner Element

Background images can't affect layout, which forces us to define a fixed height and width to use the responsive techniques outlined above. So it's not immediately obvious how to proportionally scale the height of the <span>. The solution is to use a percentage padding-top on an inner element to provide the proportionate height (see padding on MDN).

For padding, percentages are defined relative to the parent element's width. Somewhat counter-intuitively, padding-top is also defined as a percentage of the parent element's width — not the height as you might expect. Using padding-top on an inner element allows the height to be proportionally scaled.

Below is the .image as described above with an additional .inner element.

proportional-step-1.htmlGist page
<span id="road" class="image" role="img" aria-label="A road in Sweden during winter."><span class="inner"></span></span>

Example: See a live example although it doesn't scale yet.

Step 2: Adding More Generic Styles

The .inner element needs to be set to display: block so that it can have dimensions applied. The height: 0 is required because it gets its height from padding that is applied in the <style> element. The CSS below should be added to the generic CSS created in step 3 above.

proportional.cssGist page
/* additional generic styles to be added to the styles from step 3 above */
.image .inner {
display: block;
height: 0;
}

Example: See a live example although it still doesn't scale.

Step 3: Responsive, Proportionally-Scaled Images

As mentioned above, the height of the .inner element comes from a percentage applied to padding-top. The percentage is calculated by dividing the height of the source image by the width. For instance, if an image is 200 pixels wide and 100 pixels tall, the padding-top would be 50% (100px / 200px * 100% = 50%).

The example below allows the responsive image to scale proportionally.

proportional-step-3.htmlGist page
<span class="image-scope">
<style scoped>
/* fall-back styles for browsers without media query support (IE6-8) */
#road {
background-image: url('road.jpg');
width: 100%;
/* don't define a height on the .image */
}
#road .inner {
padding-top: 66.5039%; /* 681px / 1024px * 100% = 66.5039% */
}
/* small image for mobile */
@media (min-width: 0px) {
#road {
background-image: url('road-small.jpg');
/* width of 100% is inherited from the fall-back styles */
/* don't define a height on the .image */
}
#road .inner {
padding-top: 66.5625%; /* 213px / 320px * 100% = 66.5625% */
}
}
/* medium image for tablets */
@media (min-width: 640px) {
#road {
background-image: url('road-medium.jpg');
/* width of 100% is inherited from the fall-back styles */
/* don't define a height on the .image */
}
#road .inner {
padding-top: 66.5625%; /* 426px / 640px * 100% = 66.5625% */
}
}
/* large image for desktop */
@media (min-width: 1024px) {
#road {
background-image: url('road.jpg');
/* width of 100% is inherited from the fall-back styles */
/* don't define a height on the .image */
}
#road .inner {
padding-top: 66.5039%; /* 681px / 1024px * 100% = 66.5039% */
}
}
</style>
<span id="road" class="image" role="img" aria-label="A road in Sweden during winter."><span class="inner"></span></span>
</span>

Example: See a live example that should scale proportionally as expected in browsers that support background-size.

NOTE: As mentioned above, proportional scaling will only work in browsers that support background-size. Unsupported browsers will simply show the background at full-size, centered inside the <span>. If the <span> is smaller than the image in IE8 and below, the image will appear clipped.

Conclusion

This idea is in its infancy right now but I think it may be a good alternative to the many JavaScript-based solutions out there. The positive side is that it makes it simple to supply media queries for creating responsive/responsive images and it doesn't use any JavaScript at all. The negative side is that the markup isn't 100% valid and the CSS can get a little verbose. At the same time, none of the other proposed solutions have a particularly clean syntax either. The <picture> tag requires multiple <source> tags, each with their own media query and relies on JavaScript. The srcset proposal uses an alternate syntax to media queries, is not easy to understand and also will require JavaScript to function cross-browser.

The biggest concern is how to roll out responsive images over a large organization. What happens if a large corporation upgrades their website with a new responsive design to take full advantage of the rapidly growing mobile market? Should they roll out the <picture> tag or the srcset solutions? Should they roll out a complicated <style scoped> solution like what's proposed above? No matter what path is chosen, the result is going to be slightly messy because there are no CMS that currently provide a nice solution for managing responsive images across a large organization.

Although most people are probably not going to be comfortable faking images using a <span> and an embedded <style> tag, I believe the immediate browser support and predictable behavior of this solution make it a compelling addition to the responsive image conversation.

Comments