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.