Images on today’s web can be a hassle with all the requirements for different devices, bandwidths and screen sizes. Here are some findings of my colleagues and I on developing front-end heavy websites with React and Craft CMS.
When we needed an image 10 years ago, we would just add an image tag with a source and alt attribute and that was it. Everybody could view your image perfectly fine. The internet was mainly desktop computers, with similar resolutions and internet connections (although things were even simpler 10 years before that). For example, an image looked like this:
<img src="yosemite-valley.jpg" alt="Yosemite Valley" />
And you would get an image like this:
Back to 2019. When a designer comes up with a great UI design which includes some images, it can take some time to actually build it from scratch. You want to serve as many users and devices as possible without doing a lot of work for each individual image. At Born05 we usually build sites with lots of image and video content, which requires us to build them on top of a CMS to be able to maintain everything. When using fullscreen images, which demands high quality files, high quality is different for a desktop and a mobile screen, because of size, pixels and bandwidth. This means we have more things to handle than we did before.
To maintain all this rich content, our favourite choice is Craft CMS which has awesome documentation, clean setup and a nice interface (eventually non-developers are spending most time using it). Craft CMS has good support for serving assets in different sizes, which is important when handling large images. In our case we are serving our data from our CMS through GraphQL to our React app. The examples below can be used in any React app, regardless of the source of data, but we will show some examples with Craft CMS.
Let’s start with a simple image in JSX containing a source and a title, which is the same as our image above.
Now every device shows the same giant image, which you definitely don’t want on a phone. To keep things simple, we are going to generate an image for 375px screens (retina, so double this to 750px). We’re assuming the image shows on screen width (100vw). This would give us the following image:
Since we are using a CMS as a source, we do not know what the input is, so we are also formatting the image for desktop screens to be sure the right compression and resolution is used. Also, we are using assets for every breakpoint we usually use.
This would give us an extra small image on mobile devices and the large one on desktop. This solves our first problem of showing high quality images on every device without loading way more pixels than needed.
Focus on the subject
When showing a covered image (to prevent letterboxing) on different screens it is not always possible to show the entire image and sometimes the browser crops it (fullscreen images for example). We usually set a focal point in Craft on the subject of the image. Usually this is someone’s face or just an important part of the image. The focal point requires an x and y value, which goes from 0 to 1, left to right, top to bottom.
The JSX gets the data from our backend as floats from 0 to 1, so we’ve changed this to percentages required for object-position. We could show our image in different aspect ratios, without losing focus on the subject.
Now we need to lazy load, to prevent loading unnecessary images. So we need to check which image should be visible, and keep looking after scroll and resize events. We don’t load the image until the first pixel enters the viewport. Easiest way to initially hide the image is showing a small base64 image until the right source is added, so we don’t have to toggle elements. To detect if the image intersects with the viewport, we’ll use Intersection Observer with some React bindings. For example: https://github.com/researchgate/react-intersection-observer
When the image should be loaded, we’re listening to the onLoad event and checking the complete state. As soon as the image is loaded, we’ll switch the opacity on.
Now we have a lazy-loading image, which is scaled to support devices of all sizes. One remaining problem is when this image isn’t loaded yet, the browser shows an empty block. It would be a lot nicer to show a fallback color, which represents the image. In our example this would probably be a shade of green. Since we are using Craft as a backend, we can automatically retrieve the most important color from the image using our own Color extractor plugin. You could also just add a color field, or just pass a color prop to our component regardless of which backend.
The div tag containing the image gets the background-color. This way we can make the image element invisible and fade it in when loaded.
The final component, combined with all of the above, looks like the following.
This leaves us with a complete component, ready to use for showing images. It handles source-set, object-position, lazy-loading and fallback color.
Thanks for reading!