Image source: blog.logrocket.com
Prevent image layout shifts in Sanity + Gatsby
Prevent layout shifts due to images by preserving their aspect ratio in Sanity + Gatsby.
This article covers the approach I am using for this blog to prevent layout shifts from images as they load. This website is built with Gatsby.js, as a web framework and Sanity.io, as a headless CMS.
Note: For a general approach using simple CSS and HTML, follow this article on 'Loading...'.
It all starts with the gatsby-source-sanity
plugin. The plugin helps you import all your data from your Sanity database and view it within your Gatsby site, as it changes in real-time.
Warning: The setup I have listed below, is usinggatsby@2.31.1
andgatsby-source-sanity@6.0.5
. Make sure you are using compatible versions between Gatsby and the plugin as they tend to be quite "sensitive".
Image component#
Below is the way by which I am rendering an image each time there is one coming from the CMS. In more technical terms, the <Figure>
component is supplied to the @sanity/block-content-to-react
serialiser (inside block-content.js
) to convert each Sanity image into a React component.
block-content.js
import BaseBlockContent from '@sanity/block-content-to-react'
import Figure from './figure'
const serializers = {
types: {
// other types...
figure (props) {
return <Figure {...props.node} />
},
},
}
const BlockContent = ({ blocks }) =>
<BaseBlockContent blocks={blocks} serializers={serializers} />
figure.js
import {
buildImageObj,
getImageSizeFromRef,
getPaddingFromAspectRatio,
imageUrlFor
} from '../../lib/utils'
import styles from './figure.module.css'
function Figure (props) {
const imageDimensions = getImageSizeFromRef(props.asset._ref)
const paddingTop = getPaddingFromAspectRatio(imageDimensions.aspectRatio)
return (
<figure className={styles.root} style={{ paddingTop }}>
{props.asset && (
<img
loading='lazy'
src={imageUrlFor(buildImageObj(props))
.width(1200)
.maxHeight(600)
.url()}
/>
)}
</figure>
)}
To learn more about how to integrate block content and Rich Text from Sanity CMS into a React project, follow the official documentation here.
Steps outlined#
Now the above should render your images as you are including them into your Sanity studio. If you noticed above inside figure.js
lines 10 and 11, there are a couple of helper functions that are actually doing all the magic for our padding hack!
Before diving into those helper functions, let's outline the steps below and look at each one of them separately:
- Style the two main elements:
<img>
and<figure>
- Get image size information - including its aspect ratio
- Calculate
padding
value using the aspect ratio - Add the styles to image's parent element (
<figure>
in this case)
Step 1: Styles#
figure.module.css
/* <figure> styles */
.root {
margin: 2rem 0;
padding: 0;
position: relative;
width: 100%;
/* <img> styles */
@nest & img {
object-fit: contain;
position: absolute;
top: 0;
left: 0;
right: 0;
max-height: 600px;
max-width: 100%;
margin: 0 auto;
display: block;
}
}
Step 2: Get image size info#
In this hacky step, I am extracting the image dimension information from the URL string of the Sanity image using a RegExp
:
utils.js
export function getImageSizeFromRef (ref) {
const dimensionsString = /(\d+)x(\d+)/g.exec(ref)
const width = dimensionsString[1]
const height = dimensionsString[2]
const dimensions = dimensionsString[0]
const aspectRatio = height / width
return {
dimensions,
width,
height,
aspectRatio
}
}
Step 3: Calculate padding
#
In the example below, I am checking whether the aspect ratio of an image is more than 1, meaning its height
is greater than its width
, and I calculate the maximum padding
accordingly. This is in place because I have a rule that all article images should be at a maximum 600px
high.
utils.js
export function getPaddingFromAspectRatio (aspectRatio) {
return aspectRatio > 1
? `min(calc(${aspectRatio * 100}%, 600px)`
: `${Math.min(aspectRatio * 100, 100)}%`
}
Tip:100%
forpadding
, is taking the100%
value of its parentwidth
. If the parent element (i.e.<main>
) of your image wrapper element (<figure>
in this case) haspadding
and/ormargin
, make sure to account for that also!