October 14, 2019 - Code

Lightweight Image component with Gatsby JS and Contentful

An image component that serves webp, png, responsive sizes, title and alt attributes with skeleton / animated loading in minimal effort..

A hand holding polaroids

I was using Gatsby's image component for awhile, It's a great component which does a lot of the heavy lifting for you.. resizing, compression, lazy loading etc. The only thing was their styling kept messing with my styles and layout. That and I was using contentful which also does a lot of the same work for you. I decided to see if I could put together my own component which would remove the need for 2 - 3 extra npm packages required by Gatsby Image.

What we wanted to achieve with this component:

  1. Serve webp, png based on browser support.

  2. Serve responsive images using the src set attributes

  3. Serve SEO friendly title and alt tags to the images.

  4. Provided lazy loading / animations for better UX

GraphQL and Contentful

Let's tackle the first requirement which is getting the correct graphQL call to contentful grabbing everything we need. Your call might be slightly different, this is grabbing all the blog posts from Contentful. You'll notice that we are grabbing title, description, the fluid version of the image, src, srcSet and srcSetWebp.

query {
  allContentfulBlogPost {
    totalCount
    edges {
      node {
        image {
          title
          description
          fluid {
            src
            srcSet
            srcSetWebp
          }
        }
      }
    }
  }
}

What it returns should be similar to this:

{
   "data":{
      "allContentfulBlogPost":{
         "totalCount":10,
         "edges":[
            {
               "node":{
                  "image":{
                     "title":"Ocean",
                     "description":"Crashing waves",
                     "fluid":{        "src":"//images.ctfassets.net/u50cuo3eswts/3vWh7gTOIS6KDq5mYQL3vC/38cf0be1126a7fc430c15aa72f3579cd/max-sandelin-U1kG21UZg_M-unsplash.jpg?w=800&q=50",              "srcSet":"//images.ctfassets.net/u50cuo3eswts/3vWh7gTOIS6KDq5mYQL3vC/38cf0be1126a7fc430c15aa72f3579cd/max-sandelin-U1kG21UZg_M-unsplash.jpg?w=200&h=133&q=50 200w,\n//images.ctfassets.net/u50cuo3eswts/3vWh7gTOIS6KDq5mYQL3vC/38cf0be1126a7fc430c15aa72f3579cd/max-sandelin-U1kG21UZg_M-unsplash.jpg?w=400&h=267&q=50 400w,\n//images.ctfassets.net/u50cuo3eswts/3vWh7gTOIS6KDq5mYQL3vC/38cf0be1126a7fc430c15aa72f3579cd/max-sandelin-U1kG21UZg_M-unsplash.jpg?w=800&h=533&q=50 800w,\n//images.ctfassets.net/u50cuo3eswts/3vWh7gTOIS6KDq5mYQL3vC/38cf0be1126a7fc430c15aa72f3579cd/max-sandelin-U1kG21UZg_M-unsplash.jpg?w=1200&h=800&q=50 1200w,\n//images.ctfassets.net/u50cuo3eswts/3vWh7gTOIS6KDq5mYQL3vC/38cf0be1126a7fc430c15aa72f3579cd/max-sandelin-U1kG21UZg_M-unsplash.jpg?w=1600&h=1067&q=50 1600w,\n//images.ctfassets.net/u50cuo3eswts/3vWh7gTOIS6KDq5mYQL3vC/38cf0be1126a7fc430c15aa72f3579cd/max-sandelin-U1kG21UZg_M-unsplash.jpg?w=2400&h=1600&q=50 2400w,\n//images.ctfassets.net/u50cuo3eswts/3vWh7gTOIS6KDq5mYQL3vC/38cf0be1126a7fc430c15aa72f3579cd/max-sandelin-U1kG21UZg_M-unsplash.jpg?w=2560&h=1707&q=50 2560w",              "srcSetWebp":"//images.ctfassets.net/u50cuo3eswts/3vWh7gTOIS6KDq5mYQL3vC/38cf0be1126a7fc430c15aa72f3579cd/max-sandelin-U1kG21UZg_M-unsplash.jpg?w=200&h=133&q=50&fm=webp 200w,\n//images.ctfassets.net/u50cuo3eswts/3vWh7gTOIS6KDq5mYQL3vC/38cf0be1126a7fc430c15aa72f3579cd/max-sandelin-U1kG21UZg_M-unsplash.jpg?w=400&h=267&q=50&fm=webp 400w,\n//images.ctfassets.net/u50cuo3eswts/3vWh7gTOIS6KDq5mYQL3vC/38cf0be1126a7fc430c15aa72f3579cd/max-sandelin-U1kG21UZg_M-unsplash.jpg?w=800&h=533&q=50&fm=webp 800w,\n//images.ctfassets.net/u50cuo3eswts/3vWh7gTOIS6KDq5mYQL3vC/38cf0be1126a7fc430c15aa72f3579cd/max-sandelin-U1kG21UZg_M-unsplash.jpg?w=1200&h=800&q=50&fm=webp 1200w,\n//images.ctfassets.net/u50cuo3eswts/3vWh7gTOIS6KDq5mYQL3vC/38cf0be1126a7fc430c15aa72f3579cd/max-sandelin-U1kG21UZg_M-unsplash.jpg?w=1600&h=1067&q=50&fm=webp 1600w,\n//images.ctfassets.net/u50cuo3eswts/3vWh7gTOIS6KDq5mYQL3vC/38cf0be1126a7fc430c15aa72f3579cd/max-sandelin-U1kG21UZg_M-unsplash.jpg?w=2400&h=1600&q=50&fm=webp 2400w,\n//images.ctfassets.net/u50cuo3eswts/3vWh7gTOIS6KDq5mYQL3vC/38cf0be1126a7fc430c15aa72f3579cd/max-sandelin-U1kG21UZg_M-unsplash.jpg?w=2560&h=1707&q=50&fm=webp 2560w"
                     }
                  }
               }
            }
         ]
      }
   }
}

This will ensure we cover all use cases for all browsers that support webp or not and also title and alt tags for each image.

React Component

Next let's move onto the component itself. Heres the code:

import React from "react"
import styles from "./scss/image.module.scss"
const Image = ({ image }) => {
  const element = Object.keys(image).includes("fluid") ? (
    <picture>
      <source srcSet={image.fluid.srcSetWebp} type="image/webp" />
      <source srcSet={image.fluid.srcSet} type="image/png" />
      <img
        loading="lazy"
        src={image.fluid.src}
        title={image.title}
        alt={image.description}
        className={styles.animate}
      />
    </picture>
  ) : (
    <picture>
      <source srcSet={image.fixed.srcSetWebp} type="image/webp" />
      <source srcSet={image.fixed.srcSet} type="image/png" />
      <img
        loading="lazy"
        src={image.fixed.src}
        title={image.title}
        alt={image.description}
        className={styles.animate}
      />
    </picture>
  )
  return element
}
export default Image

Let's run through this code and explain whats happening.

  1. Were passing down and de-structuring the image we get from Contentful.

  2. Because Contentful can either serve fluid (full width) or fixed (fixed width) images we need to do a check for what was passed down.

  3. Then we use semantic HTML elements with picture, source and img that use all the info passed down appropriately.

  4. Notice we used the loading="lazy" attribute on, this has virtually no support except for chrome as of (12/10/2019). It doesn't hurt to add it though so that's what we did.

  5. Return the element.

As you can see, its pretty simple and doesn't require much code at all.

To use, simply pass down the entire image from graphQL, heres a real world use case based on the above graphQL call:

{
  data.allContentfulBlogPost.edges.map((edge, index) => {
    const { title, image, description } = edge.node
    return (
      <article key={index} className={styles.post}>
        <Link to={`/blog/${slugify(title)}`}>
          <Image image={image} />
          <h4 className={styles.title}>{title}</h4>
          <p className={styles.description}>{description}</p>
        </Link>
        <Link to={`/blog/${slugify(title)}`} className={styles.link}>
          Read more
        </Link>
      </article>
    )
  })
}

Styling

Now onto styling the component, I personally love skeleton loading where the UI element has a faded grey sort of 'shimmering' background while it's loading, it delivers better UX in my opinion. We included an animate class on the img element itself. Heres the css for that:

Credit to Dhilip kumar for the CSS.

.animate {
    animation: shimmer 5s infinite;
    background: linear-gradient(to right, #eff1f3 4%, #e2e2e2 25%, #eff1f3 36%);
    background-size: 1000px 100%;
}
@keyframes shimmer {
    0% {
        background-position: -1000px 0;
    }
    100% {
        background-position: 1000px 0;
    }
}

The finished product!

Heres what your component should look like when its loading for the first time:

Loading GIF

0 Comments
Leave a comment