├── 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 |
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 |
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 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------