Skip to content
Sanity.io and Gatsby.js logos

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 using gatsby@2.31.1 and gatsby-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:

  1. Style the two main elements: <img> and <figure>
  2. Get image size information - including its aspect ratio
  3. Calculate padding value using the aspect ratio
  4. 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% for padding, is taking the 100% value of its parent width. If the parent element (i.e. <main>) of your image wrapper element (<figure> in this case) has padding and/or margin, make sure to account for that also!