├── responsive-product-image.liquid
├── examples
└── collection-with-layout.liquid
├── responsive-images.js
└── README.md
/responsive-product-image.liquid:
--------------------------------------------------------------------------------
1 | {% comment %}
2 | Responsive product images with srcset.
3 | {% endcomment %}
4 |
5 | {% assign default_default_size = "medium" %}
6 | {% assign default_sizes = "100vw" %}
7 |
8 | {% if default_size == nil %}{% assign default_size = default_default_size %}{% endif %}
9 | {% if sizes == nil %}{% assign sizes = default_sizes %}{% endif %}
10 |
11 |
24 |
--------------------------------------------------------------------------------
/examples/collection-with-layout.liquid:
--------------------------------------------------------------------------------
1 | {% comment %}
2 | Shopify Responsive Images Example
3 | https://github.com/zacwasielewski/shopify-responsive-images
4 |
5 | Grid classes and breakpoints were sustainably harvested from
6 | the Timber framework (https://github.com/Shopify/Timber)
7 | {% endcomment %}
8 |
9 |
10 | Shopify Responsive Images Example
11 |
12 | {% comment %}
13 | Step 1: Include Picturefill
14 | {% endcomment %}
15 |
16 |
17 |
18 |
19 |
20 |
21 | {{ collection.title }}
22 |
23 |
24 |
50 |
51 | {% comment %}
52 | Step 2: Include responsive-images.js
53 | {% endcomment %}
54 | {{ 'responsive-images.js' | asset_url | script_tag }}
55 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/responsive-images.js:
--------------------------------------------------------------------------------
1 | var ShopifyResponsiveImages = (function( window, undefined ) {
2 |
3 | var sizes = {
4 | "small": 100,
5 | "compact": 160,
6 | "medium": 240,
7 | "large": 480,
8 | "grande": 600,
9 | "1024x1024": 1024
10 | };
11 |
12 | function load() {
13 | ready(init);
14 | }
15 |
16 | function init() {
17 | var images = document.querySelectorAll('img[data-js-responsive]');
18 | forEach(images, function(index, img) {
19 | img.addEventListener('load', function() {
20 | var scaledImage = scaleImgSrcset(this);
21 | //triggerPictureFill(scaledImage);
22 | }, false);
23 | });
24 | window.addEventListener('load', function() {
25 | //triggerPictureFill();
26 | }, false);
27 | }
28 |
29 | function scaleImgSrcset(img) {
30 | var srcset,
31 | srcset_orig,
32 | srcset_json,
33 | ratio = img.naturalWidth / img.naturalHeight,
34 | scale = getImageScale(img),
35 | is_portrait = (ratio < 1);
36 |
37 | if (!is_portrait) { return; }
38 | if (img.getAttribute('data-js-srcset-scaled') === 'true') { return; }
39 |
40 | // parse the original srcset attribute into a JSON object
41 | srcset_orig = img.getAttribute('srcset') || (img.picturefill || {}).srcset;
42 | srcset_json = srcsetToJSON(srcset_orig);
43 |
44 | // scale srcset sizes and convert back to a string
45 | srcset = srcset_json.map(function(rule) {
46 | return scaleSrcsetRule(rule, scale);
47 | }).join(',');
48 |
49 | img.setAttribute('srcset', srcset);
50 | img.setAttribute('data-js-srcset-scaled', 'true'); // prevent rescaling if
load event is triggered again
51 |
52 | return img;
53 | }
54 |
55 | function triggerPictureFill(img) {
56 | if (typeof picturefill === 'undefined') { return; }
57 | if (img) {
58 | picturefill({ reevaluate: true, elements: [img] });
59 | } else {
60 | picturefill({ reevaluate: true });
61 | }
62 | }
63 |
64 | function getImageScale(img) {
65 |
66 | var longest_edge = Math.max(img.naturalWidth, img.naturalHeight);
67 | for (var key in sizes) {
68 | var size = sizes[key];
69 | if (img.src.indexOf( "_" +key+ "." )) {
70 | if (size > longest_edge) {
71 | return longest_edge / size;
72 | }
73 | }
74 | }
75 | return 1;
76 | }
77 |
78 | function srcsetToJSON(srcset) {
79 | return srcset.split(',').map(function(rule){
80 | return rule.trim().split(/\s+/).map(function(s) { return s.trim(); });
81 | });
82 | }
83 |
84 | function scaleSrcsetRule(rule, scale) {
85 | var url = rule[0],
86 | width = parseInt(rule[1].slice(0,-1));
87 | if (width > 160) { width = Math.floor(width * scale); } // don't scale square thumbnails
88 | return [url, width.toString()+"w"].join(" ");
89 | }
90 |
91 | // Utility methods
92 |
93 | function ready(fn) {
94 | if (document.readyState != 'loading'){
95 | fn();
96 | } else {
97 | document.addEventListener('DOMContentLoaded', fn);
98 | }
99 | }
100 |
101 | function forEach(array, callback, scope) {
102 | for (var i = 0; i < array.length; i++) {
103 | callback.call(scope, i, array[i]);
104 | }
105 | }
106 |
107 | // Public methods
108 |
109 | return {
110 | load: load
111 | };
112 |
113 | })( window );
114 |
115 | ShopifyResponsiveImages.load(); // undefined
116 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Responsive Shopify product images
2 | ===
3 |
4 | Use this `.liquid` snippet in your Shopify theme to generate responsive product images.
5 |
6 | Quick start
7 | ---
8 |
9 | 1. Copy `responsive-product-image.liquid` to your theme's `snippets/` folder and include it wherever you would normally include a product image. You *must* include the `image` parameter, specifying which product image to use:
10 |
11 | ```ruby
12 | {% include 'responsive-product-image',
13 | image: product.featured_image %}
14 | ```
15 |
16 | 2. Copy `responsive-images.js` to `assets/` and include it with your other scripts (usually at the bottom of `theme.liquid`):
17 |
18 | ```ruby
19 | {{ 'responsive-images.js' | asset_url | script_tag }}
20 | ```
21 |
22 | That will output:
23 |
24 | ```html
25 |
36 | ```
37 |
38 | Advanced usage
39 | ---
40 |
41 | The snippet above is fine, but you'll get better results by estimating how much real estate your images will occupy at specific screen sizes.
42 |
43 | For example, let's assume your theme has a typical responsive product grid layout that changes depending on the browser size:
44 |
45 | | Screen size | Grid columns | % of browser width |
46 | |--------|--------------:|--------------------:|
47 | |Mobile (<= 480px)|1|100%|
48 | |Tablet (> 480px and <= 768px)|2|50%|
49 | |Desktop (> 768px)|4|25%|
50 |
51 | You would give the `sizes` option this list of media queries (`vw` stands for viewport width):
52 |
53 | ```ruby
54 | {% include 'responsive-product-image',
55 | image: product.featured_image,
56 | sizes: '(max-width: 480px) 100vw, (min-width: 481px) and (max-width: 768px) 50vw, 25vw'%}
57 | ```
58 |
59 | More about `srcset` and `sizes`:
60 | - https://dev.opera.com/articles/native-responsive-images/
61 |
62 | Options
63 | ---
64 |
65 | | Option | Description | Default |
66 | |--------|-------------|---------|
67 | | `image` | Product image to display | **None (required)**
68 | | `sizes` | Media queries describing how large the image will be at various screen sizes. | `'100vw'` |
69 | | `default_size` | [Shopify image size](https://docs.shopify.com/themes/liquid-documentation/filters/url-filters#size-parameters) to load as default `
` src | `'medium'` |
70 | | `attributes` | String of additional HTML attributes for the `
` tag | `''` |
71 |
72 | FAQ
73 | ---
74 |
75 | - **Why should I use responsive images?**
76 |
77 | Your site will load faster. With responsive images, the user's web browser chooses to download the most appropriate image size for the device and layout.
78 |
79 | - **What's up with that chunk of Javascript?**
80 |
81 | It's a necessary hack. Shopify unfortunately doesn't tell us the width of an image, only the length of its *longest side*. But `srcset` *must* know an image's width to work accurately. So as a workaround, that bit of Javascript analyzes the image as soon as it loads, determine its actual width, and updates the srcset attribute.
82 |
83 | - **Will this work in all browsers?**
84 |
85 | Yes. However, [native `srcset` support](http://caniuse.com/#feat=srcset) is relatively new, so if you need to support older browsers, then also include the [Picturefill](http://scottjehl.github.io/picturefill/) library.
86 |
87 | Reponsive image resources
88 | ---
89 |
90 | - http://alistapart.com/article/responsive-images-in-practice
91 | - http://ericportis.com/posts/2014/srcset-sizes/
92 | - http://martinwolf.org/2014/05/07/the-new-srcset-and-sizes-explained/
93 | - http://dev.opera.com/articles/native-responsive-images/
94 |
--------------------------------------------------------------------------------