The Blog

Our latest insights, opinions and general musings as we float about on the vast ocean of tech we call home.

A comprehensive guide to images in Gatsby

A comprehensive guide to images in Gatsby

As noted by Kyle Gill in his popular "Image Optimization Made Easy with Gatsby.js" post, ensuring that images used on the modern web are best suited for the end user in terms of screen size and connection speed, can be an arduous task. But it's a task that can bring huge benefits to the performance of your website and by extension the experience of your users.

Gatsby and a number of plugins in its rich ecosystem take a lot of the pain out of image optimisation. In this guide we'll look at a number of these plugins in detail with the aim of making this your go-to guide for working with images in a Gatsby site.

gatsby-image

The gatsby-image plugin is the key to working with images in any Gatsby site. It exposes a React component, which we'll take a look at shortly, and builds upon a couple of other plugins. To get started you'll need to install gatsby-image itself as well as those peer dependencies:

npm install --save gatsby-image
npm install --save gatsby-plugin-sharp
npm install --save gatsby-transformer-sharp

The "sharp" plugin and transformer use the Sharp image processing library to generate optimised versions of images at build time. The resulting images are added to Gatsby's world as nodes which means you can reference them in your GraphQL queries.

Now that the plugins are installed we need to tell Gatsby to use them. We also need to let it know where it can find our images in cases where they're not co-located with files that are already being processed by Gatsby. We can do all this in gatsby-config.js:

module.exports = {
  plugins: [
    // This configuration assumes images are all stored in the "images" directory
    // in your project root. Configure gatsby-source-filesystem multiple times if
    // you have images in many places.
    {
      resolve: 'gatsby-source-filesystem',
      options: {
        path: `${__dirname}/images`,
        name: 'images',
      },
    },
    'gatsby-transformer-sharp',
    'gatsby-plugin-sharp',
  ],
};

With that set up we're now in a position to query for optimised images in our GraphQL queries. This example assumes the above configuration as well as an image located at images/example.png relative to the project root.

export const query = graphql`
  query {
    file(absolutePath: {
      regex: "/\\/images\\/example\\.png/"
    }) {
      childImageSharp {
        fixed(width: 800) {
          ...GatsbyImageSharpFixed
        }
      }
    }
  }
`;

There's a few things to understand here. We're querying for a file node at a given absolute path. We're then extracting a child node of type ImageSharp and within that we're querying for fixed. Finally, we're extracting whatever fields are referenced by the GatsbyImageSharpFixed fragment. Let's dig into these things a little more.

Fixed vs. fluid

The gatsby-transformer-sharp plugin is responsible for creating nodes of type ImageSharp from files. It, and the gatsby-image plugin, then adds a number of utilities on top. The fixed query and the fragment in the code above are examples of this. With gatsby-image you can think of all images as falling into one of two categories:

  • Fixed. An image with a fixed width and height. An image of this type needs to exist in a number of pre-determined sizes so that an appropriate size can be used for a given screen resolution.

  • Fluid. An image designed to stretch to fill its container. Once again, a number of images at different maximum sizes are necessary.

Both of these categories are exposed by gatsby-image as GraphQL queries, named fixed and fluid respectively. The fixed query accepts width and height arguments while the fluid query accepts maxWidth and maxHeight. Each of these queries returns a node with a number of useful properties. To save you the trouble of having to remember them and write them all out every time you need to grab an image, gatsby-image provides some fragments that will extract everything you need. This is what the response might look like (using our jellyfish logo as an example):

{
  "data": {
    "file": {
      "childImageSharp": {
        "fixed": {
          "base64": "data:image/png;base64,iVBORw0KGg...kJggg==",
          "width": 10,
          "height": 10,
          "src": "/static/jellyfish-d42549e0e6ce0c81f07328255a91d82f-a88fe.png",
          "srcSet": "/static/jellyfish-d42549e0e6ce0c81f07328255a91d82f-a88fe.png 1x,\n/static/jellyfish-d42549e0e6ce0c81f07328255a91d82f-593a9.png 1.5x,\n/static/jellyfish-d42549e0e6ce0c81f07328255a91d82f-211e6.png 2x,\n/static/jellyfish-d42549e0e6ce0c81f07328255a91d82f-e9011.png 3x"
        }
      }
    }
  }
}

The gatsby-image React component

Now that we have a bunch of data related to our optimised image we need a way to actually use it. That's where the gatsby-image React component comes into play. It's effectively a drop-in replacement for the native <img> tag, but it expects the result of an ImageSharp fixed or fluid query rather than the usual src attribute. Here's an example component making use of the output of the fixed query from above:

import React from 'react';
import Image from 'gatsby-image';

export default ({ data }) => (
  <Image fixed={data.file.childImageSharp.fixed} alt="Jellyfish" />
);

The component accepts a range of props, many of which are passed on to the rendered <img> element (such as alt, shown in the example above). This results in markup similar to the following:

<div class=" gatsby-image-wrapper" style="position: relative; overflow: hidden; display: inline-block; width: 60px; height: 60px;">
  <img alt="Jellyfish" src="data:image/png;base64,iVBORw0KGg...kJggg==" style="position: absolute; top: 0px; left: 0px; width: 100%; height: 100%; object-fit: cover; object-position: center center; opacity: 0; transition: opacity 0.5s ease 0.5s;">
  <picture>
    <source srcset="/static/jellyfish-d42549e0e6ce0c81f07328255a91d82f-4ca09.png 1x, /static/jellyfish-d42549e0e6ce0c81f07328255a91d82f-475ff.png 1.5x, /static/jellyfish-d42549e0e6ce0c81f07328255a91d82f-580b9.png 2x">
    <img alt="Jellyfish" width="60" height="60" src="/static/jellyfish-d42549e0e6ce0c81f07328255a91d82f-4ca09.png" style="position: absolute; top: 0px; left: 0px; width: 100%; height: 100%; object-fit: cover; object-position: center center; opacity: 1; transition: opacity 0.5s ease 0s;">
  </picture>
  <noscript>
    <picture>
      <source srcSet="/static/jellyfish-d42549e0e6ce0c81f07328255a91d82f-4ca09.png 1x, /static/jellyfish-d42549e0e6ce0c81f07328255a91d82f-475ff.png 1.5x, /static/jellyfish-d42549e0e6ce0c81f07328255a91d82f-580b9.png 2x" />
      <img width="60" height="60" src="/static/jellyfish-d42549e0e6ce0c81f07328255a91d82f-4ca09.png" alt="Jellyfish" style="position:absolute;top:0;left:0;transition:opacity 0.5s;transition-delay:0.5s;opacity:1;width:100%;height:100%;object-fit:cover;object-position:center"/>
    </picture>
  </noscript>
</div>

You can imagine the amount of time and effort it would take to manually set up this kind of thing for every image on your site. Not a bad return on investment when we let Gatsby handle it for us!

Images in Markdown

So far we've seen how to directly get optimised images with GraphQL but many Gatsby sites use Markdown as a content format which can include images. Happily, there's another plugin that we can use to get those images optimised as before. This one is called gatsby-remark-images and it also relies upon the Sharp library to generate the desired copies of any images it comes across. It's a special kind of plugin that actually connects to another plugin rather than Gatsby itself. The following example gatsby-config.js assumes you already have the gatsby-transformer-remark plugin installed.

module.exports = {
  plugins: [
    'gatsby-plugin-sharp',
    {
      resolve: 'gatsby-transformer-remark',
      options: {
        plugins: [
          {
            resolve: 'gatsby-remark-images',
            options: {
              maxWidth: 970,
            },
          },
        ],
      },
    },
  ],
};

This allows you to use images in Markdown documents as follows:

Images in Markdown are similar to links:

![alt text](/images/example.png)

Behind the scenes this results in something similar to our previous GraphQL examples. When generating a static page from a Markdown file, Gatsby will produce a set of optimised images and output code that covers a wide range of cases.

Image references in Markdown frontmatter

As well as images referenced directly in your content, a common technique is to use a property in frontmatter. You might see this approach used for "hero" images in blog posts for example. In this case, you need to be careful to use relative (to the Markdown file) paths for Gatsby to detect and create the correct node type. For example, given a blog post at posts/example.md and an image at images/example-hero.png you might have the following frontmatter in the Markdown:

---
title: An example blog post
hero: ../images/example-hero.png
---

Main content of the post

If any of your posts do not have a hero image you need to omit the relevant property from the frontmatter. If any of the paths used do not resolve to a file Gatsby will not create child nodes, instead leaving the value as a string.

The GraphQL query for your page will now need to be updated in a similar fashion to the first example we saw above. The hero property will have a child node of type ImageSharp. A query for blog posts by title might look something like this:

export const query = graphql`
  query BlogPostByTitle($title: String!) {
    markdownRemark(
      frontmatter: {
       title: {
          eq: $title
        }
      }
    ) {
      html
      frontmatter {
        title
        hero {
          childImageSharp {
            fluid(maxWidth: 980) {
              ...GatsbyImageSharpFluid
            }
          }
        }
      }
    }
  }
`;

Rendering the image is once again a case of using the gatsby-image React component.

Handling mixed image formats

Consider the previous example of blog post hero images once more. If you have a collection of blog posts with hero images of varying formats you may run into problems with the previous GraphQL query. The gatsby-transformer-sharp plugin will only transform JPEG, PNG, WebP and TIFF files. Notable exclusions therefore include SVG and GIF. In the case of SVGs this is because there is no need - they are responsive by default. With GIFs it's because of a limitation with the underlying Sharp image processing library. Regardless, we need a way to handle situations where we don't know which image format we're dealing with.

One solution is to expand the GraphQL query to allow us to cope with images that have been processed as well as those that have not. When gatsby-transformer-sharp cannot process an image it simply copies it into the output directory allowing us to reference it by URL. The above query could be updated as follows:

export const query = graphql`
  query BlogPostByTitle($title: String!) {
    markdownRemark(
      frontmatter: {
       title: {
          eq: $title
        }
      }
    ) {
      html
      frontmatter {
        title
        hero {
          publicURL
          childImageSharp {
            fluid(maxWidth: 980) {
              ...GatsbyImageSharpFluid
            }
          }
        }
      }
    }
  }
`;

Note the addition of the publicURL attribute alongside the childImageSharp selection. This attribute will have a value even when childImageSharp does not and we can use that value to render a normal <img> element instead of a gatsby-image component. A wrapper around the gatsby-image component makes this more straightforward in practice:

import React from 'react';
import GatsbyImage from 'gatsby-image';

export default ({ node, ...props }) => {
  if (node.childImageSharp && node.childImageSharp.fluid) {
    return <GatsbyImage fluid={node.childImageSharp.fluid} {...props} />;
  }

  if (node.childImageSharp && node.childImageSharp.fixed) {
    return <GatsbyImage fixed={node.childImageSharp.fixed} {...props} />;
  }

  return <img src={node.publicURL} {...props} />;
};

We've published this component to npm so you don't have to include it in your own codebase. Check it out on GitHub.

Want to know more?

Get in touch.