Picture of the author

Static Assets Story for your Storybook.js

Published on

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:

assets

Here we have svg images, a couple of fonts, and a video file.

Let's do this:

story

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: