Blog

Responsive Images with Craft

Published

With Craft and the picture element, you can display images optimized for different breakpoints and have more control over your responsive layouts. You’ll need Picturefill to enable the picture element in browsers that don’t support it natively. If you aren’t familiar with the picture element, the Picturefill site does a great job of explaining the markup and it’s features. 

A couple examples

Resize your browser to see the images below at each breakpoint (small, 768 pixels, and 1,280 pixels).

small-screen

An image at full-width (size1of1)

small-screen

The same image at 1/3 width (size1of3)

Each source has a standard resolution image, a retina image, and a min-width media query which determines when each image is displayed. 

<!-- .size1of1 -->
<picture>
    <!--[if IE 9]><video style="display: none;"><![endif]-->
    <source
        srcset="
            /assets/img/uploads/_1599xAUTO_crop_top-center_100/large-screen.gif,
            /assets/img/uploads/_3198xAUTO_crop_top-center_70/large-screen.gif 2x"
        media="(min-width: 1280px)"
    >
    <source
        srcset="
            /assets/img/uploads/_1279xAUTO_crop_top-center_100/medium-screen.gif,
            /assets/img/uploads/_2558xAUTO_crop_top-center_70/medium-screen.gif 2x"
        media="(min-width: 768px)"
    >
    <!--[if IE 9]></video><![endif]-->
    <img
        src="/assets/img/uploads/_1279xAUTO_crop_top-center_100/medium-screen.gif"
        srcset="
            /assets/img/uploads/_480xAUTO_crop_top-center_100/small-screen.gif,
            /assets/img/uploads/_960xAUTO_crop_top-center_70/small-screen.gif 2x"
        alt="An example showing a different image for each breakpoint" />
</picture>

<!-- .size1of3 -->
<picture>
    <!--[if IE 9]><video style="display: none;"><![endif]-->
    <source
        srcset="
            /assets/img/uploads/_533xAUTO_crop_top-center_100/large-screen.gif,
            /assets/img/uploads/_1066xAUTO_crop_top-center_70/large-screen.gif 2x"
        media="(min-width: 1280px)"
        >
        <source
            srcset="
                /assets/img/uploads/_426xAUTO_crop_top-center_100/medium-screen.gif,
                /assets/img/uploads/_852xAUTO_crop_top-center_70/medium-screen.gif 2x"
            media="(min-width: 768px)"
        >
    <!--[if IE 9]></video><![endif]-->
    <img
        src="/assets/img/uploads/_426xAUTO_crop_top-center_100/medium-screen.gif"
        srcset="
            /assets/img/uploads/_480xAUTO_crop_top-center_100/small-screen.gif,
            /assets/img/uploads/_960xAUTO_crop_top-center_70/small-screen.gif 2x"
        alt="An example showing a different image for each breakpoint" />
</picture>

Putting it together with Craft

picture-block

You’ll need the following fields to get started:

  • Image - an asset field used to set the default image
  • Size — a dropdown used to set the proportional size of the image (i.e. 1, 12, 13, etc.)

Image will be displayed at each breakpoint, unless a specific image is associated for small or large screens, and for browsers that don’t support media queries. Craft makes it easy to associate images optimized for different screen sizes by adding a field to the asset source for each breakpoint where you want the ability to add an optimized image.

  • Small - an asset field used to serve a specific image on small screens
  • Large — an asset field used to serve a specific image on large screens
picture-popover

Determining crop sizes

Before we can display the image sources, we need to know each breakpoint our site addresses and crop the image to a size appropriate for each breakpoint. You might need a 3,200 pixel wide image to serve a full-width image on a retina screen, but that same image might only be seen at 320 pixels wide on a mobile device. To provide some level of optimization and ensure it remains clear throughout the breakpoint, the image is cropped to the maximum width it will be displayed at for each breakpoint.

Setting breakpoints

Craft lets you configure variables that are available sitewide in craft/config/general.php. Setting an array of breakpoints with minWidth and maxWidth dimensions makes it simple to set sources in the picture element with crop parameters for each source and the media query used to determine which source should be displayed. The array below can be accessed in any template using craft.config.breakpoints.

return array(
   '*' => array(
      'breakpoints' => array(
         'large'  => array(
            'minWidth'  => 1280,
            'maxWidth'  => 1599,
         ),
         'medium'  => array(
            'minWidth'  => 768,
            'maxWidth'  => 1279,
         ),
         'small'  => array(
            'minWidth'  => 1,
            'maxWidth'  => 767,
         ),
      ),
   ),
   …
);

Sizes

I use a grid based on Nicole Sullivan’s OOCSS (where sizes are based on proportion to the row rather than the number of columns spanned). In general.php I set another array with each column size and its corresponding fraction. Multiplying the size by the max-width of the breakpoint determines the width to crop the image for each source in the picture element.

return array(
   '*' => array(
      'breakpoints' => array(
         …
      ),
      'sizes'  => array(
         'size1of1' => 1,
         'size1of2' => 1/2,
         'size1of3' => 1/3,
         'size2of3' => 2/3,
         'size1of4' => 1/4,
         'size3of4' => 3/4,
         'size1of5' => 1/5,
         'size2of5' => 2/5,
         'size3of5' => 3/5,
         'size4of5' => 4/5,
         'size1of6' => 1/6,
         'size5of6' => 5/6
      )
   ),
);

Template

All the heavy lifting is done in a template named picture” which loops through each breakpoint and sets breakpointImage as the source for that breakpoint. While looping, check to see if a field matching the breakpoint’s name is set and if so check to see if an image has been added to that field. If not, the template uses the image from the parent field (Image).

{# Image object #}
{%- set image = image is defined ? image : null -%}
{# The size of the column the image is inside, used to determine the crop width) #}
{%- set size = size is defined ? size : "size1of1" -%}
{# Breakpoints and their minWidth and maxWidth in pixels, declared in general.php #}
{%- set breakpoints = breakpoints is defined ? breakpoints : (craft.config.breakpoints is defined ? craft.config.breakpoints : {}) -%}
{# The site’s column sizes and their corresponding fractions, declared in general.php #}
{%- set sizes = sizes is defined ? sizes : (craft.config.sizes is defined ? craft.config.sizes : {}) -%}
{# `srcset` for the `img` element #}
{%- set srcset = "" -%}
{# Fallback `src` for the `img` element #}
{%- set src = "" -%}
{% if image|length %}
<picture>
    {# Output a standard and retina source for each breakpoint #}
    {% for breakpoint,width in breakpoints %}
        {# Images sized specifically for each breakpoint can be uploaded using a field on the
        ## asset named for the breakpoint.
        ##
        ## An image for small screens would be uploaded to the `image.small` field on the asset
        #}
        {%- set breakpointImage = image[breakpoint] is defined and image[breakpoint]|length ?
            image[breakpoint].first() : image -%}
        …
    {% endfor %}
…
</picture>
{% endif %}

Inside that same loop, crop parameters are set for a standard and retina version of the image. For small screens I ignore the size value for the image and set the cropWidth based on the smallest breakpoint’s maxWidth since columns will be full-width on these screens. For larger screens the cropWidth is determined by multiplying the breakpoint’s maxWidth by the size value. For example, images for medium screens are cropped to 1,279 pixels and shown on viewports 768 pixels wide up to 1,279 pixels wide.

{% if image|length %}
<picture>
    {# Output a standard and retina source for each breakpoint #}
    {% for breakpoint,width in breakpoints %}
        …
        {# Set parameters for images at each breakpoint
        ## The width is calculated by multiplying the
        ## pixel size of the breakpoint by the size of the wrapper
        ## element.
        ##
        ## An image in a `size1of2` wrapper element at the _medium_
        ## breakpoint where the `max-width` is 1279px:
        ## 1279 * 1/2 = 639.5
        ##
        ## (For now images at the _small_ breakpoint are cropped based on the `max-width`
        ## at 100% width)
        #}
        {%- set cropWidth =  (breakpoint == "small" ?
            width["maxWidth"] : (width["maxWidth"] * sizes[size|e])|round) -%}
        {%- set params = {
            "1x": {
                mode: 'crop',
                width: cropWidth,
                quality: 100,
                position: 'top-center'
            },
            "2x": {
                mode: 'crop',
                width: cropWidth * 2,
                quality: 70,
                position: 'top-center'
            }
        } -%}
        …
    {% endfor %}
…
</picture>
{% endif %}

Each source is added to the picture element (except for the smallest breakpoint which will have its srcset added to the img element). Additionally, while setting the source for the medium breakpoint, the fallback src attribute for the img element is set.

{% if image|length %}
<picture>
    {% for breakpoint,width in breakpoints %}
        …
        {% if breakpoint == "small" %}
            {# Set scrset for smallest breakpoint and output on <img /> tag later #}
            {%- set srcset = breakpointImage.getUrl(params["1x"]) ~ ", " ~ breakpointImage.getUrl(params["2x"]) ~ " 2x" -%}
        {% else %}
            {% if breakpoint == "medium" %}
                {# Set image for legacy browsers #}
                {%- set src = breakpointImage.getUrl(params["1x"]) -%}
            {% endif %}
            {# Source is shown at breakpoint’s `min-width` using an image cropped
            ## to the breakpoint’s `max-width` value so images are crisp #}
            <source
                srcset="
                    {{ breakpointImage.getUrl(params["1x"]) }},
                    {{ breakpointImage.getUrl(params["2x"]) }} 2x"
                media="(min-width: {{ width["minWidth"] }}px)"
            >
        {% endif %}
    {% endfor %}
…
</picture>
{% endif %}

Finally, the img element is output with the smallest breakpoint’s srcset and the fallback src attribute for browsers that don’t support media queries.

{% if image|length %}
<picture>
    {% for breakpoint,width in breakpoints %}
        …
    {% endfor %}
    <img
        src="{{ src }}"
        {% if srcset|length %}
            srcset="{{ srcset }}"
        {% endif %}
        {%- if image.alt is defined -%}
            alt="{{ image.alt }}" />
        {%- endif -%}
</picture>
{% endif %}

The final template

{%- set image = image is defined ? image : null -%}
{%- set size = size is defined ? size : "size1of1" -%}
{# Breakpoints declared in general.php #}
{%- set breakpoints = breakpoints is defined ? breakpoints : (craft.config.breakpoints is defined ? craft.config.breakpoints : {}) -%}
{# Sizes declared in general.php #}
{%- set sizes = sizes is defined ? sizes : (craft.config.sizes is defined ? craft.config.sizes : {}) -%}
{%- set srcset = "" -%}
{%- set src = "" -%}
{% if image|length %}
<picture>
    <!--[if IE 9]><video style="display: none;"><![endif]-->
    {# Output a standard and retina source for each breakpoint #}
    {% for breakpoint,width in breakpoints %}
        {# Images sized specifically for each breakpoint can be uploaded using a field on the
        ## asset named for the breakpoint.
        ##
        ## An image for small screens should be uploaded to the `image.small` field on the asset
        #}
        {%- set breakpointImage = image[breakpoint] is defined and image[breakpoint]|length ?
            image[breakpoint].first() : image -%}
        {# Set parameters for images at each breakpoint
        ## The width is calculated by multiplying the
        ## pixel size of the breakpoint by the size of the wrapper
        ## element.
        ##
        ## An image in a `size1of2` wrapper element at the _medium_
        ## breakpoint where the `max-width` is 1279px:
        ## 1279 * 1/2 = 639.5
        ##
        ## (For now images at the _small_ breakpoint are cropped based on the `max-width`
        ## at 100% width)
        #}
        {%- set cropWidth =  (breakpoint == "small" ?
            width["maxWidth"] : (width["maxWidth"] * sizes[size|e])|round) -%}
        {%- set params = {
            "1x": {
                mode: 'crop',
                width: cropWidth,
                quality: 100,
                position: 'top-center'
            },
            "2x": {
                mode: 'crop',
                width: cropWidth * 2,
                quality: 70,
                position: 'top-center'
            }
        } -%}
        {% if breakpoint == "small" %}
            {# Set scrset for smallest breakpoint and output on <img /> tag later #}
            {%- set srcset = breakpointImage.getUrl(params["1x"]) ~ ", " ~ breakpointImage.getUrl(params["2x"]) ~ " 2x" -%}
        {% else %}
            {% if breakpoint == "medium" %}
                {# Set image for legacy browsers #}
                {%- set src = breakpointImage.getUrl(params["1x"]) -%}
            {% endif %}
            {# Source is shown at breakpoint’s `min-width` using an image cropped
            ## to the breakpoint’s `max-width` value so images are clear #}
            <source
                srcset="
                    {{ breakpointImage.getUrl(params["1x"]) }},
                    {{ breakpointImage.getUrl(params["2x"]) }} 2x"
                media="(min-width: {{ width["minWidth"] }}px)"
            >
        {% endif %}
    {% endfor %}
    <!--[if IE 9]></video><![endif]-->
    <img
        src="{{ src }}"
        {% if srcset|length %}
            srcset="{{ srcset }}"
        {% endif %}
        {%- if image.alt is defined -%}
            alt="{{ image.alt }}" />
        {%- endif -%}
</picture>
{% endif %}

Write that once and include everywhere you need a picture element with this one short snippet:

{% include "_includes/markup/picture" with {
    image: image,
    size: size
} %}

Please send along any suggestions, corrections, or improvements via email or Twitter.