Static Assets Story for your Storybook.js
Almost every project has static assets such as images, videos, fonts, etc. These are often placed in a public
or static
folder and used throughout the application.
In Storybook, you can create a story for these static assets to conveniently view and use them in app components.
In this article, I will provide an example for both Storybook builders: @storybook/builder-webpack5
and @storybook/builder-vite
(https://storybook.js.org/docs/builders), and I will use React (the code can be easily adapted for any library/framework supported by Storybook.js).
As an example, let's take the following set of static resources in the public/assets
folder:
Here we have svg images, a couple of fonts, and a video file.
Let's do this:
As data, we will use an array of objects where each object contains information about a static asset: file path, resource URL, filename, and type (image, video, font):
path: String
url: String
filename: String
isImage: Boolean
isVideo: Boolean
isFont: Boolean
The filename can be obtained from the path, and the type can be determined by checking the file extension:
{
filename: path.split("/").pop(),
isImage: /\.(png|jpe?g|gif|svg|avif)$/.test(path),
isVideo: /\.(mp4|webm|ogg)$/.test(path),
isFont: /\.(woff2?|eot|ttf|otf)$/.test(path),
}
The key point is to get the path
and url
for each resource. This is done differently for Vite and Webpack.
Vite
In Vite, this can be done using import.meta.glob
:
const modules = import.meta.glob(["/public/assets/**/*.*"], {
query: "?url",
import: "default",
eager: true,
});
const assets = Object.entries(modules).map(([path, url]) => ({
path,
url: url.replace(/^\/public/, ""),
filename: path.split("/").pop(),
isImage: /\.(png|jpe?g|gif|svg|avif)$/.test(path),
isVideo: /\.(mp4|webm|ogg)$/.test(path),
isFont: /\.(woff2?|eot|ttf|otf)$/.test(path),
}));
Webpack
In Webpack, you can use require.context
:
function importAll(r) {
return r.keys().reduce((acc, path) => ({ ...acc, [path]: r(path) }), {});
}
const modules = importAll(require.context("/public/assets", true));
const assets = Object.entries(modules).map(([path, url]) => ({
path,
url,
filename: path.split("/").pop(),
isImage: /\.(png|jpe?g|gif|svg|avif)$/.test(path),
isVideo: /\.(mp4|webm|ogg)$/.test(path),
isFont: /\.(woff2?|eot|ttf|otf)$/.test(path),
}));
Writing a Story
Now let's write the UI.
To display an image, you can use the <img src={url} />
tag, for video - <video src={url} muted autoPlay loop />
, and for fonts it is a bit trickier - <span>AaBbCc</span>
, plus we will need to add @font-face
and set the corresponding font-family
.
We will also support opening the resource in a new tab with a link to the resource.
Here's the component I came up with:
<div className="p-3">
<ul className="grid list-none grid-cols-[repeat(auto-fill,minmax(120px,1fr))] gap-4">
{assets.map(({ path, url, filename, isImage, isVideo, isFont }) => (
<li
key={path}
className="grid grid-rows-[1fr_auto] items-center justify-items-center p-1 border border-gray-200 rounded-md gap-1 relative group"
>
{isImage && <img src={url} alt="" />}
{isVideo && <video src={url} muted autoPlay loop />}
{isFont && (
<div>
<style>
{`@font-face {
font-family: "${filename}";
src: url("${url}");
}`}
</style>
<span
style={{ fontFamily: `"${filename}"` }}
className="text-xl"
>
AaBbCc
<br />
012345
</span>
</div>
)}
<code className="text-xs text-nowrap text-ellipsis overflow-hidden max-w-full">
{filename}
</code>
<a
href={url}
target="_blank"
rel="noreferrer"
className="absolute p-2 top-0 right-0 rounded-md leading-none opacity-0 group-hover:opacity-100 focus:opacity-100 text-gray-400 hover:text-black focus:text-black"
aria-label="Open in new tab"
>
↗
</a>
</li>
))}
</ul>
</div>
Demo:
You can see the full code in the repository https://github.com/nag5000/storybook_static_assets_story: