In this post, we will be setting up Next.js and Storybook in a way that makes it possible to use Next.js' Image component in components rendered inside Storybook.
Introduction
If you want to take a look at the final product first, or if you don't care about the step-by-step, here's the accompanying repo.
Rendering a component that uses the Next.js Image component inside Storybook might have you running into a few gotchas. I myself ran into two:
- Storybook can't seem to find the image you statically import from your
public
directory, resulting in the following error:
Failed to parse src "static/media/public/<imageName>.jpg" on `next/image`, if using relative image it must start with a leading slash "/" or be an absolute URL (http:// or https://)
- Storybook can't find the blur placeholder image that is automatically generated by Next.js and injected into the
blurDataURL
prop, resulting in the following error:
Image with src "static/media/public/<imageName>.jpg" has "placeholder='blur'" property but is missing the "blurDataURL" property.
The reasons for both of these are actually the same: when run inside Storybook, your code won't run through Next.js' build process, during which the correct URLs/Paths and alternative sizes for your hosted images as well as the placeholder images are created and injected into your code.
If you know how, both of these are straightforward to solve. But finding the solutions can be kind of an odyssey. So here they are in one place.
Note: in this post, I assume you have already set up both Next.js as well as Storybook. If not, do that and then come back here.
Storybook Can't Find Images Imported from Next.js' public
Directory
Let's assume you have the following component:
// src/components/ImageTest.js
import Image from "next/image"
import testImage from "../public/testImage.jpg"
const ImageTest = () => (
<Image src={testImage}
alt="A stack of colorful cans"
layout="fill"
/>
)
export default ImageTest
And the following story for it:
// stories/ImageTest.stories.js
import React from 'react';
import ImageTest from '../components/ImageTest';
export default {
title: 'Image/ImageTest',
component: ImageTest,
};
const Template = (args) => <ImageTest {...args} />;
export const KitchenSink = Template.bind({});
KitchenSink.args = {};
If you were to run Storybook now, this would be what you saw:
When I first encountered this, I reasoned Storybook simply couldn't find the assets in Next.js' public/
directory. But running with the -s public/
command line option to tell it about the directory doesn't solve the problem.
After some digging, the issue here seems to be the things going on behind the scenes of the Image
component. One of its most useful features is that it will automatically optimize the image you pass it and create and serve alternative sizings of it on demand. Next.js can't work its magic when the Image
component is rendered inside Storybook though, and that's why the solution here is to simply turn these optimizations off in this context. To do this, we'll have to add the following to Storybook's setup code:
// .storybook/preview.js
import * as NextImage from "next/image";
const OriginalNextImage = NextImage.default;
Object.defineProperty(NextImage, "default", {
configurable: true,
value: (props) => (
<OriginalNextImage
{...props}
unoptimized
/>
),
});
This code will replace the Image
component's default export with our own version, which adds the unoptimized
prop to every instance of it. With this, "the source image will be served as-is instead of changing quality, size, or format", according to the Next.js documentation. And since we added this to Storybook's setup code, this will only be done when our component is rendered inside Storybook.
Credit for this solution goes to Github user rajansolanki, who synthesized a few prior solution attempts in this comment on the relevant Github issue.
If you want to read more about Next.js' Image
component and the props you can pass into it, take a look at the introduction to its features as well as its documentation.
Storybook Can't Find the Blur Placeholder Image That is Automatically Generated by Next.js and Injected Into the blurDataURL
Prop
Another nice feature of the Image
component is that it will automatically generate small, blurred placeholder images for display during the loading of the full image.
All we need to do to activate this feature is to pass the placeholder="blur"
prop:
// src/components/ImageTest.js
import Image from "next/image"
import testImage from "../public/testImage.jpg"
const ImageTest = () => (
<Image src={testImage}
alt="A stack of colorful cans"
layout="fill"
placeholder="blur" // this is new!
/>
)
export default ImageTest
But this will immediately result in the following error when we run Storybook again:
The reason for this is basically the same as before. Next.js will generate a placeholder image and inject it into the component for us. So that one line of code we added actually does a whole lot in the background, all of which, again, is not automatically done when run inside Storybook. Luckily the solution is a one liner as well:
// .storybook/preview.js
import * as NextImage from "next/image";
const OriginalNextImage = NextImage.default;
Object.defineProperty(NextImage, "default", {
configurable: true,
value: (props) => (
<OriginalNextImage
{...props}
unoptimized
// this is new!
blurDataURL="data:image/jpeg;base64,/9j/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAADAAQDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAf/xAAbEAADAAMBAQAAAAAAAAAAAAABAgMABAURUf/EABUBAQEAAAAAAAAAAAAAAAAAAAMF/8QAFxEAAwEAAAAAAAAAAAAAAAAAAAECEf/aAAwDAQACEQMRAD8Anz9voy1dCI2mectSE5ioFCqia+KCwJ8HzGMZPqJb1oPEf//Z"
/>
),
});
What we have done here is a bit of a hack, granted. But it's effective, as all good hacks are. With this, we're setting all placeholder images to be the same data, at least in the context of Storybook. The string above is actually the base64 representation of the placeholder for the plaiceholder example image on their homepage. But we could just as easily upload our own image there and use that.
And with this, you should see the following when running Storybook:
Note: our testImage for this is this image by Studio Blackthorns on Unsplash.
If you want to read more about the automatic placeholder generation of Next.js' Image
component, take a look at the placeholder
prop's documentation.
Don't forget that you can use or take a look at the accompanying repo.
💡 This post was published 2021/07/21 and last updated 2021/07/21. Since node libraries change and break all the time, there's a non-trivial chance that all of this is obsolete and none of it works when you read this in your flying taxi, gulpin' on insect protein shakes sometime in the distant future or, let's be honest here, next week. If so, tell me in the comments and I'll see if an update would still be relevant.
Top comments (20)
Very, very helpful! I'm also using Sanity.io as a CMS and had to use a fallback prop since I'm not passing in JSON data for each image. If anyone else wants to know what I did, I just used the OR operator like so:
Hey! Thank you so much! You've saved me a lot of time
Glad it helped!
Works like a charm, thanks.
This was a life-saver, thanks!
Thank you very much for sharing.It was really helpful.
Very helpful post. I struggled to find solution for 3 hours but no other blog could help me. I solved after reading this post.
Many thanks Jonas!!! :)
It's not working for me
Something might have changed since I wrote the post. Can you tell me more specifics? What is not working exactly? Do you get any error messages? Then I will try to update the post.
Didn't work for me either. What worked for me is providing defaultProps with unoptimized = true in "preview.js"
Thank you both, guys! Oleg's tweak was needed as well in to make it work properly ("next": "12.1.6" + "@storybook/react": "^6.5.9")
Hey! Sorry, I'm currently working with neither next nor storybook anymore and frankly, don't have the time right now to go into this and comprehend what is going on. Still, this post seems to be pretty popular and I'd like it to be at least correct and runnable when people find it. Would you be willing to suggest the necessary changes to my post to make it map to what you had to do? I would then change the necessary parts and mark you as contributors. Would be really cool!
Thank you for sharing, Jonas. I ran into this problem and your article helped me a lot!
Hey Rafael, really glad to hear that! If you run into any other problems related to this, please tell me about them and your solution and I will add them to the post (with attribution to you of course).
The
-s public/
seems to be deprecated.Use
staticDirs: ['../public']
in main.js instead.Very helpful! Thank you!