├── LICENSE ├── _img.twig └── README.md /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Marion Newlevant 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /_img.twig: -------------------------------------------------------------------------------- 1 | {# macros for responsive images. #} 2 | 3 | {% macro _classAttr(classes) %} 4 | {%- if (classes|length) -%} 5 | class="{{ classes|join(' ') }}" 6 | {%- endif -%} 7 | {% endmacro %} 8 | 9 | {% macro fixedSize(asset, width, options={}) %} 10 | {% import _self as self %} 11 | {% set options = { 12 | alt: asset.altText, 13 | class: [] 14 | }| merge(options) %} 15 | 16 | {% set transform = { 17 | mode: 'stretch' 18 | } %} 19 | 20 | {% set nativeWidth = asset.getWidth(false) %} 21 | 22 | {{options.alt}} 40 | {% endmacro %} 41 | 42 | {% macro responsive(asset, options={}) %} 43 | {% import _self as self %} 44 | {% set options = { 45 | alt: asset.altText, 46 | class: [], 47 | style: 'default' 48 | }| merge(options) %} 49 | 50 | {% set transform = { 51 | mode: 'stretch' 52 | } %} 53 | 54 | {% set nativeWidth = asset.getWidth(false) %} 55 | 56 | {# 57 | # Here is where you configure the image styles. 58 | # You are going to have to modify this for your 59 | # individual site. 60 | # 61 | # config is a hash, where the key is the style, 62 | # and the value is another hash 63 | # of srcsetWidths, sizes, and defaultWidth. 64 | # 65 | # There should always be a 'default' style. 66 | # Redefine the 'default' to whatever makes sense 67 | # for you, and add other styles as needed. 68 | # 69 | # srcsetWidths: image widths that should appear 70 | # in the srcset. 71 | # sizes: media queries that specify the widths 72 | # of the image at different screen widths. 73 | # The first one that matches is used. 74 | # defaultWidth: image width for the src image 75 | # (fallback for browsers that don't understand 76 | # srcset) 77 | #} 78 | {% set config = { 79 | default: { 80 | srcsetWidths: [400, 800, 1000], 81 | sizes: [ 82 | '(max-width: 30rem) 100vw', 83 | '25em' 84 | ], 85 | defaultWidth: 800 86 | }, 87 | thumb: { 88 | srcsetWidths: [200, 400], 89 | sizes: [ 90 | '200px' 91 | ], 92 | defaultWidth: 200 93 | } 94 | } %} 95 | 96 | {% set params = config[options.style] %} 97 | 98 | {% set srcset = [asset.getUrl(false)~' '~nativeWidth~'w'] %} 99 | 100 | {% for width in params['srcsetWidths'] %} 101 | {% if width < nativeWidth %} 102 | {% set srcset 103 | = srcset|merge([asset.getUrl(transform|merge({width: width}))~' '~width~'w']) 104 | %} 105 | {% endif %} 106 | {% endfor %} 107 | 108 | {{options.alt}} 122 | {% endmacro %} 123 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This article will discuss responsive image markup and twig macros to automate generating that markup. 2 | 3 | There are three things I hope you will get from this article: 4 | 5 | 1. A quick overview of responsive images (and links to further resources). 6 | 2. A useful twig macro for generating responsive image markup. 7 | 3. Nerdy details about programming in twig, useful when you write your own twig macros. 8 | 9 | ## Responsive Images 10 | 11 | Here are some excellent articles with information on responsive images. If you know nothing about responsive images, read at least one of them, and then come back: 12 | - [Figuring Out Responsive Images](https://css-tricks.com/video-screencasts/133-figuring-responsive-images/), _CSS-Tricks_ 13 | - [Responsive Images Done Right: A Guide To <picture> And srcset](http://www.smashingmagazine.com/2014/05/14/responsive-images-done-right-guide-picture-srcset/) _Smashing Magazine_ 14 | - [Srcset and sizes](https://ericportis.com/posts/2014/srcset-sizes/), _Eric Portis_ 15 | - [Responsive Images in Practice](http://alistapart.com/article/responsive-images-in-practice), _A List Apart_ 16 | - [Picturefill polyfill](http://scottjehl.github.io/picturefill/), _Picturefill_ 17 | 18 | There are a number of responsive image use cases. This article will focus on two, both of which use the `img` tag (and not the `picture` tag). 19 | 20 | ### Fixed-width images 21 | 22 | The first use case is a fixed-width image (so not actually responsive...) that adapts to different device-pixel-ratios. Say you have a thumbnail image which is always displayed 200px wide, and you have versions for 1x (thumb.jpg) and 2x (thumb_2x.jpg). The markup for this is: 23 | 24 | ``` html 25 | thumb 30 | ``` 31 | 32 | Browsers that don't understand `srcset` will ignore that attribute, and download `thumb.jpg`. Browsers that do understand `srcset` will use `thumb.jpg` on devices with `1x` resolution, and `thumb_2x.jpg` on devices with `2x` resolution (or higher). 33 | 34 | ### Variable-width images 35 | 36 | The second use case is variable-width images with no art direction (so the only difference in the images is the resolution). Say you have an image that will be full-width on narrower screens (<= 30em), and 25em wide on larger screens. And you have this image available in three different sizes: 37 | 38 | - illo_small.jpg is 400px 39 | - illo_medium.jpg is 800px 40 | - illo_large.jpg is 1000px 41 | 42 | The markup for this is: 43 | 44 | ``` html 45 | illustration 51 | ``` 52 | 53 | Browsers that don't understand `srcset` and `sizes` will use the 800px version. Browsers that do understand `srcset` and `sizes` will know from the `srcset` attribute that the image is available in those three sizes, and from the `sizes` attribute that if the browser window is up to 30em the image will be sized at `100vw` (which is 100% of the viewport width), and otherwise it will be `25em`. At this point it is up to the browser to decide which one to download. 54 | 55 | ## A very brief review of twig macros 56 | 57 | Here is a simple [twig macro](http://twig.sensiolabs.org/doc/tags/macro.html) definition: 58 | 59 | ``` twig 60 | {% macro greet(name='World') %} 61 | Hello, {{name}}! 62 | {% endmacro %} 63 | ``` 64 | Every macro begins with the `{% macro %}` tag, and ends with the `{% endmacro %}` tag. This one takes one parameter (`name`), which has a default value of `'World'`. 65 | To call it, you first need to [`import`](http://twig.sensiolabs.org/doc/tags/import.html) it. 66 | 67 | ``` twig 68 | {% macro greet(name='World') %} 69 | Hello, {{name}}! 70 | {% endmacro %} 71 | 72 | {# Import the macro into the file in which it is defined #} 73 | {% import _self as self %} 74 | 75 | {# Call your macro #} 76 | {{ self.greet() }} 77 | ``` 78 | The example above will generate `Hello, World!`. To generate a different output, you can call the macro using a different `name` parameter. 79 | 80 | ``` twig 81 | {# This will generate `Hello, Dear Reader!` #} 82 | {{ self.greet('Dear Reader') }} 83 | ``` 84 | 85 | If the macro is defined in a different file than the one where it is called, you import it (in the calling file): 86 | 87 | ``` twig 88 | {% import '_macros/_utils' as m_utils %} 89 | {# Call an imported macro using the name you imported it with #} 90 | {{ m_utils.greet() }} 91 | ``` 92 | 93 | What you name your macro files, and what you import them as are matters of convention. I use `self` when the macro is in the same file, and `m_whatever` when the macro is in the file `_macros/_whatever.twig`. 94 | 95 | ## Twig macros for responsive images 96 | 97 | A lot of the work in creating the responsive image markup can be automated. Rather than uploading the same image at different resolutions, we can use Craft's [Image Transformations](http://buildwithcraft.com/docs/image-transforms) to generate the various image sizes. 98 | 99 | I have a macro file, `_img.twig` that has macros for each of the two use cases ([available on github](https://github.com/marionnewlevant/twig-img-macro/blob/master/_img.twig)). You pass these macros an image asset, and they generate markup for a responsive image. 100 | 101 | One thing these macros do is try to be smart about which image transforms to ask Craft for. There is no point in Craft transforming an image larger than the original size. If you have a 400px image, and you need an 800px image, the browser should make that transformation. There is also no point in Craft transforming an image to the same size. If the image is originally 800px, do not transform it to 800px, just use the original image. 102 | 103 | `_img.twig` defines two public macros (and a third for use only inside the file): 104 | - **fixedSize()** - Public 105 | - **responsive()** - Public 106 | - **_classAttr()** - Internal 107 | 108 | ### Example of fixed-width image macro 109 | 110 | `fixedSize()` is for the first use case - a fixed size image that adapts to different device-pixel-ratios. Here is an example of using it: 111 | 112 | ``` twig 113 | {% import '_macros/_img' as m_img %} 114 | {% set thumbAsset = entry.assetField.first() %} 115 | 116 | {# Example of a simple call to the macro #} 117 | {{ m_img.fixedSize(thumbAsset, 200) }} 118 | 119 | {# Example of more advanced call 120 | specifying alt attribute of 'some thumb' and class of 'thumb-class' 121 | #} 122 | {{ m_img.fixedSize(thumbAsset, 200, {alt: 'some thumb', class: ['thumb-class']}) }} 123 | ``` 124 | 125 | ### Example of responsive-width image macro 126 | 127 | `responsive()` is for the second use case - a variable width image. Here is an example of using it: 128 | 129 | ``` twig 130 | {% import '_macros/_img' as m_img %} 131 | 132 | {% set illoAsset = entry.assetField.first() %} 133 | 134 | {# Example of a simple call to the macro #} 135 | {{ m_img.responsive(illoAsset) }} 136 | 137 | {# Example of another macro call using a different style #} 138 | {{ m_img.responsive(thumbAsset, {style: 'thumb'}) }} 139 | 140 | ``` 141 | This is not quite all there is to using the `responsive()` macro. You also need to define what widths the image should be avaiable in (`srcset`), and how wide the image will be displayed (`sizes`). Details of that are below. 142 | 143 | ## Taking a closer look at the _img.twig macro file 144 | 145 | Here is an annotated version of the twig macro file, `_img.twig`. The annotations follow the code they describe. 146 | 147 | ### The Class Attribute Macro: __classAttr()_ 148 | 149 | ``` twig 150 | {% macro _classAttr(classes) %} 151 | {%- if (classes|length) -%} 152 | class="{{ classes|join(' ') }}" 153 | {%- endif -%} 154 | {% endmacro %} 155 | ``` 156 | 157 | `_classAttr` is a macro intended to only be called from within `_img.twig`, which is why its name begins with `_` (this is a convention of mine, not something the language enforces). If it turns out to have wider utility, I will factor it out into a file of utility macros (and rename it to `classAttr`). 158 | 159 | `_classAttr` takes an array of class names, and returns a class attribute string. `_classAttr(['foo', 'bar'])` will return `class="foo bar"`. `_classAttr([])` will return an empty string. `{%-` and `-%}` are twig's [tag level whitespace control](http://twig.sensiolabs.org/doc/templates.html#whitespace-control), used to strip out whitespace the macro would otherwise generate. The [join](http://twig.sensiolabs.org/doc/filters/join.html) filter takes an array of strings and turns it into a string of space separated words. 160 | 161 | ### The Fixed Size Macro: _fixedSize()_ 162 | 163 | ``` twig 164 | {% macro fixedSize(asset, width, options={}) %} 165 | {% import _self as self %} 166 | ``` 167 | 168 | This is the macro for the first use case. It is passed an [asset](http://buildwithcraft.com/docs/assets), the width in `px` at which the image will be displayed, and [optionally] a hash of options. 169 | 170 | Since we are going to use the `_classAttr` macro, defined in this file, we [`import`](http://twig.sensiolabs.org/doc/tags/import.html) it. 171 | 172 | ``` twig 173 | {% set options = { 174 | alt: asset.altText, 175 | class: [] 176 | }| merge(options) %} 177 | ``` 178 | 179 | We define default options, and [`merge`](http://twig.sensiolabs.org/doc/filters/merge.html) them with the `options` which were passed in. Options passed in will override these default ones. 180 | 181 | The two options are `alt`, and `class`. `alt` will be the alt text for the tag. All my image assets have a field `altText`, which is used as the default value. `class` is an array of class names, defaulting to none. 182 | 183 | ``` twig 184 | {% set transform = { 185 | mode: 'stretch' 186 | } %} 187 | ``` 188 | 189 | We define the [transform](http://buildwithcraft.com/docs/image-transforms#defining-transforms-in-your-templates) we will use. This `transform` is missing the `width` attribute. When we actually use it, we will `merge` in the `width` attribute. So to transform to 200: `asset.getUrl(transform|merge({width: 200}))`. (The alternative would be `asset.getUrl({mode: 'stretch', width: 200})`, but I think using the `transform` variable is slightly easier to read). 190 | 191 | ``` twig 192 | {% set nativeWidth = asset.getWidth(false) %} 193 | ``` 194 | 195 | Set `nativeWidth` to the original, untransformed width of the image ([`getWidth(false)`](http://buildwithcraft.com/docs/templating/assetfilemodel#getWidth) returns the original width). 196 | 197 | ``` twig 198 | {{options.alt}} 216 | {% endmacro %} 217 | ``` 218 | 219 | Here we generate the `img` tag. First is the `src` attribute, which will be transformed down to the 1x size only if the `nativeWidth` is larger than that. Second is the `srcset` attribute. There are three cases here: 220 | 221 | - the `nativeWidth` exactly matches the 2x size - no transform 222 | - `nativeWidth` is larger than the 2x size - transform it down 223 | - `nativeWidth` is smaller than the 2x size - no `srcset` at all 224 | 225 | Finally, we have our `alt` and `class` attributes. 226 | 227 | 228 | ### The Responsive Macro: _responsive()_ 229 | 230 | ``` twig 231 | {% macro responsive(asset, options={}) %} 232 | 233 | {% import _self as self %} 234 | 235 | {% set options = { 236 | alt: asset.altText, 237 | class: [], 238 | style: 'default' 239 | }| merge(options) %} 240 | 241 | {% set transform = { 242 | mode: 'stretch' 243 | } %} 244 | 245 | {% set nativeWidth = asset.getWidth(false) %} 246 | ``` 247 | 248 | This is the macro for the second use case. It is passed an asset and an options hash. In addition to the `alt` and `class` values, there is a `style` (which defaults to `default`). 249 | 250 | The `style` is used to pick the configuration for a particular style of responsiveness from the `config` hash (defined below). 251 | 252 | We set `options`, `transform`, and `nativeWidth` just as in `fixedSize()`. 253 | 254 | ``` twig 255 | {# 256 | # Here is where you configure the image styles. 257 | # You are going to have to modify this for your 258 | # individual site. 259 | # 260 | # config is a hash, where the key is the style, 261 | # and the value is another hash 262 | # of sizes, srcsetWidths, and defaultWidth. 263 | # 264 | # There should always be a 'default' style. 265 | # Redefine the 'default' to whatever makes sense 266 | # for you, and add other styles as needed. 267 | # 268 | # srcsetWidths: image widths that should appear 269 | # in the srcset. 270 | # sizes: media queries that specify the widths 271 | # of the image at different screen widths. 272 | # The first one that matches is used. 273 | # defaultWidth: image width for the src image 274 | # (fallback for browsers that don't understand 275 | # srcset) 276 | #} 277 | 278 | {% set config = { 279 | default: { 280 | srcsetWidths: [400, 800, 1000], 281 | sizes: [ 282 | '(max-width: 30rem) 100vw', 283 | '25em' 284 | ], 285 | defaultWidth: 800 286 | }, 287 | thumb: { 288 | srcsetWidths: [200, 400], 289 | sizes: [ 290 | '200px' 291 | ], 292 | defaultWidth: 200 293 | } 294 | } %} 295 | ``` 296 | 297 | `config` will need to be modified for your particular project. Here is where you specify the widths for `srcset` and the values for `sizes`. This `default` style is the same as the one from the `illo` example earlier in the article. The `thumb` style is another way of doing the 200px fixed width thumbs. 298 | 299 | ``` twig 300 | {% set params = config[options.style] %} 301 | ``` 302 | 303 | `config` defines multiple sets of style params. Fetch the one we will use. 304 | 305 | ``` twig 306 | {% set srcset = [asset.getUrl(false)~' '~nativeWidth~'w'] %} 307 | ``` 308 | 309 | `srcset` will be an array of strings (we will use `join` to turn them into one comma separated string later). Here we initialize the array with a single string for the `nativeWidth` image, which is always included. `~` is twig's [concatenation operator](http://twig.sensiolabs.org/doc/templates.html#other-operators). We concatenate the image url, a space, the width, and the string `w`, and set `srcset` to an array of just that string. 310 | 311 | ``` twig 312 | {% for width in params['srcsetWidths'] %} 313 | {% if width < nativeWidth %} 314 | {% set srcset 315 | = srcset|merge([asset.getUrl(transform|merge({width: width}))~' '~width~'w']) 316 | %} 317 | {% endif %} 318 | {% endfor %} 319 | ``` 320 | 321 | Here we loop over the `srcsetWidths`, and add the width string to the `srcset` array for any that are less than the `nativeWidth`. 322 | 323 | ``` twig 324 | {{options.alt}} 338 | {% endmacro %} 339 | ``` 340 | 341 | Here we generate the `img` tag. First is the `src` attribute, which will be our `defaultWidth` if the image is large enough, and otherwise the `nativeWidth`. After that is the `srcset` attribute, where we `join` the `srcset` variable, this time putting `", "` between the strings. Then the `sizes` attribute, similarly constructed with `join`. And finally the `alt` and `class` attributes. 342 | 343 | ## Some Details 344 | 345 | * Responsive images are reasonably well supported by browsers (see [caniuse data](http://caniuse.com/#search=srcset) for details). Additionally, there is a [polyfill](http://scottjehl.github.io/picturefill/) which is very easy to use. Download it, and add this script tag: 346 | ``` html 347 | 348 | ``` 349 | 350 | * I always set [generateTransformsBeforePageLoad](http://buildwithcraft.com/docs/config-settings#generateTransformsBeforePageLoad) to `true` in my `config/general.php`: 351 | ``` php 352 | 'generateTransformsBeforePageLoad' => true, 353 | ``` 354 | This generates the transform when `getUrl()` is called, rather than waiting for the browser to request the image. This will make the first page load a little bit slower, but it lets you [`cache`](http://buildwithcraft.com/docs/templating/cache) the result of calling the macros. 355 | 356 | * You will need to specify the `config` styles for the `responsive()` macro, but you can wait to fill in the details of the styles until your site design is quite solid. And these values never have to be completely precise. Reasonably close is fine (though you probably want to get the breakpoint values exact). 357 | 358 | * Enjoy! Feel free to contact me with any questions: [marion.newlevant@gmail.com](mailto:marion.newlevant@gmail.com). 359 | --------------------------------------------------------------------------------