As a frontend development tool, Storybook barely needs an introduction anymore. On this blog alone, Storybook has been covered many times, with topics ranging from setup guides and different testing forms to comparisons with drop-in alternatives.
Storybook has established itself as an industry standard for developing isolated UI components and pages, enabling better, faster, and future-proof frontend development. It should come as no surprise that Storybook is used by thousands of teams every day — but such popularity also comes with certain challenges.
In this post, we’ll talk about how Storybook’s new Framework API aims to address those challenges, including:
For what Storybook tries to achieve, it’s of utmost importance that components render and behave exactly the same as they do in your production environment. If that’s not the case, all of Storybook’s benefits related to isolation, reusability, and quality go out the window.
One of Storybook’s significant challenges is supporting other libraries and frameworks in the JavaScript landscape. There are so many to take into account that it’s impossible to have an out-of-the-box, one-size-fits-all solution.
For a long time, Storybook solved this by providing a very flexible configuration that allowed users to align the Storybook configuration to their stack’s configuration no matter how elaborate it was. But this introduced a different challenge: because the default Storybook configurations needed to account for all possibilities, they could only be very barebones to avoid becoming prescriptive.
This shifted a lot of responsibility onto the developer, making the entire setup process a very time-consuming and painful one, particularly when using off-the-shelf frontend frameworks like Next.js and Gatsby. Though they come with many convenient features out of the box, including a configured development server, in the context of setting up your Storybook configurations properly, this became extremely problematic.
Many developers faced challenging setup times and configurations as a result. Any project that uses a popular framework will, to a certain extent, need the same set of configurations. However, as every project is configured slightly differently, there wasn’t a way to share these configurations between projects.
This is where the Framework API, introduced in Storybook 7, comes in. To further understand what it does, it’s useful to dive into the two different parts that Storybook sees in this whole configuration issue. These are called builders and renderers.
Builders are the tools that compile, package, and update your resources into a single bundle for the browser. This includes, but is not limited to, JavaScript, CSS, and MDX files. Some of the most common builders are webpack, esbuild, and Vite.
Renderers, on the other hand, are responsible for taking your frontend code, like components, and putting it on the screen. Some of the most common renderers out there are React, Vue, and Angular.
Storybook Framework APIs include a set of configurations for Storybook, targeting particular combinations of builders and renderers. At a base level, frameworks help Storybook to mimic your project’s build and rendering configurations.
Beyond that, the package architecture allows for convenient abstraction and easier reusability for projects that share the same builder and renderer. This is especially useful for frontend frameworks that already set both these up, like Next.js.
Optionally, Storybook Frameworks can even help out with mocking certain application features that are provided out-of-the-box by your frontend framework or renderer, such as routing- and data-fetching-related ones. We’ll see examples of this later in this article.
Credit is due to the Storybook team, as installing these new frameworks is extremely straightforward.
To enable the desired framework in your project’s Storybook configuration, you need to adjust the .storybook/main.js
file. This will tell Storybook to reference the correct framework and so it automatically picks up on the included settings.
In a lot of scenarios, this should be enough. But sometimes, the out-of-the-box configurations might not suffice because your project’s configurations are very specific or customized. In those cases, you can use any Storybook Framework as a base configuration and extend upon it using the webpackFinal
function, like so:
// .storybook/main.js export default { addons: [/* ... */], stories: [/* ... */], framework: '@storybook/nextjs', webpackFinal: async (config, { configType }) => { // Additional configurations return config; }, }
To further understand how the Framework API exactly works, we’ll have to dive into what a package consists of. As described in the official documentation, there are three main files:
preset.js
preview.js
types.ts
The preset.js
file is the main driving force behind the package. In it, you can set up all the relevant configurations for your builder and renderer. If you’ve ever set up Storybook for a project before the Framework API, this file will feel very familiar.
The main change is that it now requires you to use certain exports to configure specific parts. These exports are:
core
viteFinal
or webpackFinal
babel
addons
frameworkOptions
It’s likely that you won’t need all of them. In most scenarios, the core
and viteFinal
/webpackFinal
exports will be more than enough.
Through core
, you can specify the builder and renderer that your Storybook Framework needs. For additional configurations related to the builder, you can use viteFinal
or webpackFinal
, respectively. These two will accept a config
function just like you’re used to. For additional adjustments to the transpilation or compilation process, you can use the babel
export.
Additional Storybook-related options, like including addons or configuring the available options for your Storybook framework, can be done with the addons
and frameworkOptions
files.
The remaining two files, preview.js
and types.ts
, are optional. preview.js
is for configuring how your stories render. This includes things like (global) decorators, runtime configurations, presets, and addons. One important thing to note is that if you want to use this file, you’ll need to add a config
export to your preset.js
file and reference it.
TypeScript support is a common and very appreciated factor for frontend packages. For your Storybook framework configurations, these typings can be included through the types.ts
file.
At the time of writing this article, Storybook offers a list of official frameworks that you can use that target the following combinations of builders and renderers, or off-the-shelf frontend frameworks:
But the Storybook team isn’t leaving it at this already extensive list of frameworks. Support for Next.js and SvelteKit were only recently added, while popular frameworks like Remix and Nuxt are being considered by the Storybook team in collaboration with their respective maintenance teams.
Now that we’ve taken a look at how these frameworks work and what’s inside of them, let’s dive into some of the existing frameworks provided by the Storybook team. This should give us a feeling of how things work in the real world, how difficult it is to create a Storybook Framework, and what’s necessary to configure them properly.
We’ll start by covering two smaller and simpler examples, React with webpack and Vue with Vite, and end with a larger and more specific one, Next.js.
If we look into the framework for React with webpack, there are only the types.ts
and preset.ts
files. Starting with the former, the types.ts
file includes all the TypeScript types necessary for making use of the framework. Specifically, there are two exported types:
FrameworkOptions
, for all options related to the frameworkStorybookConfig
, for the Storybook configuration used in the main.ts
file of your Storybook setupThe preset.ts
file contains several distinct sections and exports based on what we’ve described before. As the name reflects, the most essential part of the preset.ts
file is the core
export, so let’s take a look at that.
Basically, the core
export specifies which builder and renderer the framework should use. Since the Storybook team already has packages available for React as a renderer and webpack as a builder, the framework can just reference them and Storybook will pick up on their configurations.
You can provide additional options for your builder by making the framework
entry in your configuration an object. Provide the name of the framework under a name
entry; additional builder configurations can be put under options.builder
:
// preset.ts export const core: PresetProperty<'core', StorybookConfig> = async (config, options) => { const framework = await options.presets.apply<StorybookConfig['framework']>('framework'); return { ...config, builder: { name: wrapForPnP('@storybook/builder-webpack5') as '@storybook/builder-webpack5', options: typeof framework === 'string' ? {} : framework.options.builder || {}, }, renderer: wrapForPnP('@storybook/react'), }; };
The second section, below, is about the builder and making additional adjustments to it. However, in this particular case, not much is necessary to adjust besides adding a resolve alias to make sure the @storybook/react
dependency from the framework is used:
// preset.ts export const webpack: StorybookConfig['webpack'] = async (config) => { config.resolve = config.resolve || {}; config.resolve.alias = { ...config.resolve?.alias, '@storybook/react': wrapForPnP('@storybook/react'), }; return config; };
The last section is about the Framework itself, which adds some addons and framework-related options based on React and webpack. This means you don’t have to configure all of them on your own when setting up your Storybook project, and provides you with some sensible defaults.
// preset.ts export const addons: PresetProperty<'addons', StorybookConfig> = [ wrapForPnP('@storybook/preset-react-webpack'), ]; const defaultFrameworkOptions: FrameworkOptions = { legacyRootApi: true, }; export const frameworkOptions = async ( _: never, options: Options ): Promise<StorybookConfig['framework']> => { const config = await options.presets.apply<StorybookConfig['framework']>('framework'); if (typeof config === 'string') { return { name: config, options: defaultFrameworkOptions, }; } if (typeof config === 'undefined') { return { name: wrapForPnP('@storybook/react-webpack5') as '@storybook/react-webpack5', options: defaultFrameworkOptions, }; } return { name: config.name, options: { ...defaultFrameworkOptions, ...config.options, }, }; };
Next, let’s look at the Storybook Framework for Vue 3 with Vite. Although this is a totally different combination of renderer and builder compared to React with webpack, we’ll see that the Framework setup process is extremely familiar.
Once again, we have the types.ts
and preset.ts
files. The typings file is basically an exact copy of the one we saw in the React with webpack framework; there are only minor changes to reflect that the builder is Vite and the framework we’re using this time is Vue 3.
Slightly unexpectedly, the preset.ts
file isn’t all too different, either, and is even more straightforward. Using a simplified construction, the core
export references the appropriate renderer and builder:
// preset.ts export const core: PresetProperty<'core', StorybookConfig> = { builder: '@storybook/builder-vite', renderer: '@storybook/vue3', };
Because his framework uses Vite as the builder, it uses the viteFinal
export instead of the webpack one. As with the React with webpack Framework, we also create a resolve alias for the render to ensure the one in the Framework is used. Besides that, it adds the appropriate Vue plugin for Vite, if necessary, and a docgen
plugin on top of your settings:
// preset.ts export const viteFinal: StorybookConfig['viteFinal'] = async (config, { presets }) => { const plugins: PluginOption[] = []; // Add vue plugin if not present if (!(await hasVitePlugins(config.plugins, ['vite:vue']))) { const { default: vue } = await import('@vitejs/plugin-vue'); plugins.push(vue()); } // Add docgen plugin plugins.push(vueDocgen()); return mergeConfig(config, { plugins, resolve: { alias: { vue: 'vue/dist/vue.esm-bundler.js', }, }, }); };
Contrary to the previous two frameworks, we’ll see that the one for Next.js is quite a bit more elaborate. Given the number of Next.js-specific features that exist and that the framework supports, this doesn’t necessarily come as a surprise.
However, we’ll also see that there remain a lot of similarities between the setups, especially if you draw comparisons between the React with webpack Framework. The Next.js Framework also uses React and webpack under the hood, which will become very apparent soon.
Starting with the types.ts
file, the typings that it exports are not too different from the ones we’ve seen before. There are obvious adjustments to the Framework name, but the structure is exactly the same.
However, one key addition is the possibility in the framework options to configure a path to your Next.js project’s config file, next.config.js
:
// types.ts export type FrameworkOptions = ReactOptions & { nextConfigPath?: string; builder?: BuilderOptions; };
While the previous two frameworks only shipped with types.ts
and preset.ts
files, this framework also comes with a preview.tsx
file.
The first major thing it does is include one of the necessary runtime configurations for Next.js projects, the next-head-count
meta tag. Inside this file, this tag is globally created and configured so that your Storybook instance has it available:
// preview.tsx function addNextHeadCount() { const meta = document.createElement('meta'); meta.name = 'next-head-count'; meta.content = '0'; document.head.appendChild(meta); } addNextHeadCount();
There are two more imports that configure the general Storybook project. One of them initiates the Next.js-related config from next/config
and the other creates a stub of Next.js’s widely used image
component with some sensible defaults and fallbacks.
// preview.tsx import './config/preview'; // <- Configures `next/config` // ... import './images/next-image-stub'; // ...
Lastly, it configures several decorators that are added to each story. Decorators are essentially higher-order components that wrap your existing stories with another React component or provider to add functionality.
In this case, they’re added to match Next.js’s setup. Right now, we have decorators related to:
addon-actions
)Take a look:
// preview.tsx import { RouterDecorator } from './routing/decorator'; import { StyledJsxDecorator } from './styledJsx/decorator'; // ... import { HeadManagerDecorator } from './head-manager/decorator'; export const decorators = [StyledJsxDecorator, RouterDecorator, HeadManagerDecorator];
One small thing to note is that because we’re using the preview.tsx
file, the previewAnnotations
file has to be configured in the preset.ts
file. In this case, it’s put on the config
object as follows, referencing the preview
file:
// preset.ts export const config: StorybookConfig['previewAnnotations'] = (entry = []) => [ ...entry, require.resolve('@storybook/nextjs/preview.js'), ];
Continuing with the preset.ts
file, we’ll see a few similarities with the React with webpack framework. In particular, the core
, addons
, and frameworkOptions
exports all follow a very similar structure.
The main differences can be found in the following two exports, babel
and webpackFinal
. As mentioned, these add some additional configurations for Babel and the webpack server.
For the Babel part, its main purpose is to check whether you have an existing next/babel
config and include those configurations in Storybook. Besides that, it also adds some pre-determined Babel presets that are most commonly applied to a Next.js project.
On the webpack side of things, it does very similar things: instantiating the base configuration and adding more features on top. The latter is especially interesting to look into from the side of Next.js:
// preset.ts export const webpackFinal: StorybookConfig['webpackFinal'] = async (baseConfig, options) => { // ... const nextConfig = await configureConfig({ // .. }); configureNextFont(baseConfig); configureNextImport(baseConfig); configureRuntimeNextjsVersionResolution(baseConfig); configureImports(baseConfig); configureCss(baseConfig, nextConfig); configureImages(baseConfig); configureRouting(baseConfig); configureStyledJsx(baseConfig); return baseConfig; };
In the function, we see a list of key features for Next.js projects in the form of functions. They take the existing config and add some configurations themselves to make the respective features work with Storybook.
These include some features that are put in the foreground, advertised with the framework. Most developers will consciously know about them — think of Next.js’s image components, font optimizations, the head
component, routing, and relative imports.
But it also configures some features that exist more in the background. Most people will not notice these unless they’re broken, like out-of-the-box Sass support, CSS Modules, PostCSS, styled-jsx, and runtime configurations.
Without diving too in-depth into what every single feature configuration does, all of them do a similar thing in which they load or add the necessary webpack configurations. As an example, configureImages
adds additional webpack rules to handle the image assets and load them using a custom image loader stub.
In this article, we’ve taken a deep dive into Storybook Framework API. It addresses arguably one of the most time-consuming and tedious parts of using Storybook: setting it up to perfectly match your project’s builder and renderer configurations. Beyond looking into the reasons why we need the Framework API, what it does, and how it works, we’ve also taken a detailed look at the code of three Storybook Frameworks for popular stacks.
When comparing the Frameworks for React with webpack, Vue 3 with Vite, and Next.js, a lot of similarities arise in terms of their general structure and minimal necessities. On the other hand, we also see the differences in how detailed you can adjust your framework based on your project’s needs, as is especially apparent in the Next.js Framework. This information, together with the rest of this article, should provide you with enough knowledge to get started with the Framework API and create your own.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
Bypass anti-bot measures in Node.js with curl-impersonate. Learn how it mimics browsers to overcome bot detection for web scraping.
Handle frontend data discrepancies with eventual consistency using WebSockets, Docker Compose, and practical code examples.
Efficient initializing is crucial to smooth-running websites. One way to optimize that process is through lazy initialization in Rust 1.80.