Storybook for Next.js
Table of Contents
Supported Features
π Next.js's Image Component
π Next.js Font Optimization
π Next.js Routing (next/router)
π Next.js Head (next/head)
π Next.js Navigation (next/navigation)
π Sass/Scss
π Css/Sass/Scss Modules
π Styled JSX
π Postcss
π Absolute Imports
π Runtime Config
π Custom Webpack Config
π Typescript (already supported out of the box by Storybook)
Requirements
Getting Started
In a project without Storybook
Follow the prompts after running this command in your Next.js project's root directory:
npx storybook@next init
More on getting started with Storybook
In a project with Storybook
This framework is designed to work with Storybook 7. If youβre not already using v7, upgrade with this command:
npx storybook@next upgrade --prerelease
Automatic migration
When running the upgrade
command above, you should get a prompt asking you to migrate to @storybook/nextjs
, which should handle everything for you. In case that auto-migration does not work for your project, refer to the manual migration below.
Manual migration
Install the framework:
yarn add --dev @storybook/nextjs@next
Update your main.js
to change the framework property:
export default {
framework: {
name: '@storybook/nextjs',
options: {},
},
};
If you were using Storybook plugins to integrate with Next.js, those are no longer necessary when using this framework and can be removed:
export default {
addons: [
],
};
Documentation
Options
You can be pass an options object for addional configuration if needed.
For example:
import * as path from 'path';
export default {
framework: {
name: '@storybook/nextjs',
options: {
nextConfigPath: path.resolve(__dirname, '../next.config.js'),
},
},
};
nextConfigPath
: The absolute path to the next.config.js
Next.js's Image Component
next/image is notoriously difficult to get working with Storybook. This framework allows you to use Next.js's Image
component with no configuration!
Local Images
Local images work just fine! Keep in mind that this feature was only added in Next.js v11.
import Image from 'next/image';
import profilePic from '../public/me.png';
function Home() {
return (
<>
<h1>My Homepage</h1>
<Image
src={profilePic}
alt="Picture of the author"
// width={500} automatically provided
// height={500} automatically provided
// blurDataURL="../public/me.png" set to equal the image itself (for this framework)
// placeholder="blur" // Optional blur-up while loading
/>
<p>Welcome to my homepage!</p>
</>
);
}
Remote Images
Remote images also work just fine!
import Image from 'next/image';
export default function Home() {
return (
<>
<h1>My Homepage</h1>
<Image src="/me.png" alt="Picture of the author" width={500} height={500} />
<p>Welcome to my homepage!</p>
</>
);
}
AVIF
This format is not supported by this framework yet. Feel free to open up an issue if this is something you want to see.
Next.js Font Optimization
@next/font is partially supported in Storybook. The packages @next/font/google
and @next/font/local
are supported.
@next/font/google
You don't have to do anything. @next/font/google
is supported out of the box.
@next/font/local
For local fonts you have to define the src property.
The path is relative to the directory where the font loader function is called.
If the following component defines your localFont like this:
import localFont from '@next/font/local';
const localRubikStorm = localFont({ src: './fonts/RubikStorm-Regular.ttf' });
You have to tell Storybook where the fonts
directory is located. The from
value is relative to the .storybook
directory. The to
value is relative to the execution context of Storybook. Very likely it is the root of your project.
export default {
...
"staticDirs": [
{
from: '../src/components/fonts',
to: 'src/components/fonts'
}
],
}
Not supported features of @next/font
The following features are not supported (yet). Support for these features might be planned for the future:
Next.js Routing
Next.js's router is automatically stubbed for you so that when the router is interacted with, all of its interactions are automatically logged to the Actions ctions panel if you have the Storybook actions addon.
When using Next.js 13+, you should only use next/router
in the pages
directory. In the app
directory, it is necessary to use next/navigation
.
Overriding defaults
Per-story overrides can be done by adding a nextjs.router
property onto the story parameters. The framework will shallowly merge whatever you put here into the router.
import SomeComponentThatUsesTheRouter from './SomeComponentThatUsesTheRouter';
export default {
component: SomeComponentThatUsesTheRouter,
};
export const Example = {
parameters: {
nextjs: {
router: {
path: '/profile/[id]',
asPath: '/profile/1',
query: {
id: '1',
},
},
},
},
};
Global Defaults
Global defaults can be set in preview.js and will be shallowly merged with the default router.
export const parameters = {
nextjs: {
router: {
path: '/some-default-path',
asPath: '/some-default-path',
query: {},
},
},
};
Default Router
The default values on the stubbed router are as follows (see globals for more details on how globals work)
const defaultRouter = {
push(...args) {
action('nextRouter.push')(...args);
return Promise.resolve(true);
},
replace(...args) {
action('nextRouter.replace')(...args);
return Promise.resolve(true);
},
reload(...args) {
action('nextRouter.reload')(...args);
},
back(...args) {
action('nextRouter.back')(...args);
},
forward() {
action('nextRouter.forward')();
},
prefetch(...args) {
action('nextRouter.prefetch')(...args);
return Promise.resolve();
},
beforePopState(...args) {
action('nextRouter.beforePopState')(...args);
},
events: {
on(...args) {
action('nextRouter.events.on')(...args);
},
off(...args) {
action('nextRouter.events.off')(...args);
},
emit(...args) {
action('nextRouter.events.emit')(...args);
},
},
locale: globals?.locale,
asPath: '/',
basePath: '/',
isFallback: false,
isLocaleDomain: false,
isReady: true,
isPreview: false,
route: '/',
pathname: '/',
query: {},
};
Actions Integration Caveats
If you override a function, you lose the automatic actions integration and have to build it out yourself.
export const parameters = {
nextjs: {
router: {
push() {
},
},
},
};
Doing this yourself looks something like this (make sure you install the @storybook/addon-actions
package):
import { action } from '@storybook/addon-actions';
export const parameters = {
nextjs: {
router: {
push(...args) {
action('nextRouter.push')(...args);
return Promise.resolve(true);
},
},
},
};
Next.js Navigation
Please note that next/navigation can only be used in components/pages in the app
directory of Next.js 13+.
Set nextjs.appDirectory
to true
If your story imports components that use next/navigation
, you need to set the parameter nextjs.appDirectory
to true
in your Story:
import SomeComponentThatUsesTheNavigation from './SomeComponentThatUsesTheNavigation';
export default {
component: SomeComponentThatUsesTheNavigation,
};
export const Example = {
parameters: {
nextjs: {
appDirectory: true,
},
},
},
If your Next.js project uses the app
directory for every page (in other words, it does not have a pages
directory), you can set the parameter nextjs.appDirectory
to true
in the preview.js file to apply it to all stories.
export const parameters = {
nextjs: {
appDirectory: true,
},
};
The parameter nextjs.appDirectory
defaults to false
if not set.
Overriding defaults
Per-story overrides can be done by adding a nextjs.navigation
property onto the story parameters. The framework will shallowly merge whatever you put here into the router.
import SomeComponentThatUsesTheNavigation from './SomeComponentThatUsesTheNavigation';
export default {
component: SomeComponentThatUsesTheNavigation,
};
export const Example = {
parameters: {
nextjs: {
appDirectory: true,
navigation: {
pathname: '/profile',
query: {
user: '1',
},
},
},
},
};
Global Defaults
Global defaults can be set in preview.js and will be shallowly merged with the default router.
export const parameters = {
nextjs: {
appDirectory: true,
navigation: {
pathname: '/some-default-path',
},
},
};
useSelectedLayoutSegment
and useSelectedLayoutSegments
hook
The useSelectedLayoutSegment
and useSelectedLayoutSegments
hooks are supported in Storybook. You have to set the nextjs.navigation.segments
parameter to return the segments you want to use.
import SomeComponentThatUsesTheNavigation from './SomeComponentThatUsesTheNavigation';
export default {
component: SomeComponentThatUsesTheNavigation,
parameters: {
nextjs: {
appDirectory: true,
navigation: {
segments: ['dashboard', 'analytics']
},
},
},
};
export const Example = {};
import { useSelectedLayoutSegment, useSelectedLayoutSegments } from 'next/navigation';
export default function SomeComponentThatUsesTheNavigation() {
const segment = useSelectedLayoutSegment();
const segments = useSelectedLayoutSegments();
...
}
The default value of nextjs.navigation.segments
is []
if not set.
Default Navigation Context
The default values on the stubbed navigation context are as follows:
const defaultNavigationContext = {
push(...args) {
action('nextNavigation.push')(...args);
},
replace(...args) {
action('nextNavigation.replace')(...args);
},
forward(...args) {
action('nextNavigation.forward')(...args);
},
back(...args) {
action('nextNavigation.back')(...args);
},
prefetch(...args) {
action('nextNavigation.prefetch')(...args);
},
refresh: () => {
action('nextNavigation.refresh')();
},
pathname: '/',
query: {},
};
Actions Integration Caveats
If you override a function, you lose the automatic action tab integration and have to build it out yourself.
export const parameters = {
nextjs: {
appDirectory: true,
navigation: {
push() {
},
},
},
};
Doing this yourself looks something like this (make sure you install the @storybook/addon-actions
package):
import { action } from '@storybook/addon-actions';
export const parameters = {
nextjs: {
appDirectory: true,
navigation: {
push(...args) {
action('nextNavigation.push')(...args);
return Promise.resolve(true);
},
},
},
};
Next.js Head
next/head is supported out of the box. You can use it in your stories like you would in your Next.js application. Please keep in mind, that the Head children are placed into the head element of the iframe that Storybook uses to render your stories.
Sass/Scss
Global sass/scss stylesheets are supported without any additional configuration as well. Just import them into preview.js
import '../styles/globals.scss';
This will automatically include any of your custom sass configurations in your next.config.js
file.
import * as path from 'path';
export default {
sassOptions: {
includePaths: [path.join(__dirname, 'styles')],
},
};
Css/Sass/Scss Modules
css modules work as expected.
import styles from './Button.module.css';
export function Button() {
return (
<button type="button" className={styles.error}>
Destroy
</button>
);
}
Styled JSX
The built in CSS-in-JS solution for Next.js is styled-jsx, and this framework supports that out of the box too, zero config.
function HelloWorld() {
return (
<div>
Hello world
<p>scoped!</p>
<style jsx>{`
p {
color: blue;
}
div {
background: red;
}
@media (max-width: 600px) {
div {
background: blue;
}
}
`}</style>
<style global jsx>{`
body {
background: black;
}
`}</style>
</div>
);
}
export default HelloWorld;
You can use your own babel config too. This is an example of how you can customize styled-jsx.
{
"presets": [
[
"next/babel",
{
"styled-jsx": {
"plugins": ["@styled-jsx/plugin-sass"]
}
}
]
]
}
Postcss
Next.js lets you customize postcss config. Thus this framework will automatically handle your postcss config for you.
This allows for cool things like zero config tailwindcss! (See Next.js' example)
Absolute Imports
Goodbye ../
! Absolute imports from the root directory work just fine.
import Button from 'components/button';
import styles from 'styles/HomePage.module.css';
export default function HomePage() {
return (
<>
<h1 className={styles.title}>Hello World</h1>
<Button />
</>
);
}
Also OK for global styles in preview.js
!
import 'styles/globals.scss';
Runtime Config
Next.js allows for Runtime Configuration which lets you import a handy getConfig
function to get certain configuration defined in your next.config.js
file at runtime.
In the context of Storybook with this framework, you can expect Next.js's Runtime Configuration feature to work just fine.
Note, because Storybook doesn't server render your components, your components will only see what they normally see on the client side (i.e. they won't see serverRuntimeConfig
but will see publicRuntimeConfig
).
For example, consider the following Next.js config:
module.exports = {
serverRuntimeConfig: {
mySecret: 'secret',
secondSecret: process.env.SECOND_SECRET,
},
publicRuntimeConfig: {
staticFolder: '/static',
},
};
Calls to getConfig
would return the following object when called within Storybook:
{
"serverRuntimeConfig": {},
"publicRuntimeConfig": {
"staticFolder": "/static"
}
}
Custom Webpack Config
Next.js comes with a lot of things for free out of the box like sass support, but sometimes you add custom webpack config modifications to Next.js. This framework takes care of most of the webpack modifications you would want to add. If Next.js supports a feature out of the box, then that feature will work out of the box in Storybook. If Next.js doesn't support something out of the box, but makes it easy to configure, then this framework will do the same for that thing for Storybook.
Any webpack modifications desired for Storybook should be made in .storybook/main.js.
Note: Not all webpack modifications are copy/paste-able between next.config.js
and .storybook/main.js
. It is recommended to do your reasearch on how to properly make your modifcation to Storybook's webpack config and on how webpack works.
Below is an example of how to add svgr support to Storybook with this framework.
export default {
webpackFinal: async (config) => {
const imageRule = config.module.rules.find((rule) => rule.test.test('.svg'));
imageRule.exclude = /\.svg$/;
config.module.rules.push({
test: /\.svg$/,
use: ['@svgr/webpack'],
});
return config;
},
};
Typescript
Storybook handles most Typescript configurations, but this framework adds additional support for Next.js's support for Absolute Imports and Module path aliases. In short, it takes into account your tsconfig.json
's baseUrl and paths. Thus, a tsconfig.json
like the one below would work out of the box.
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/components/*": ["components/*"]
}
}
}
Notes for Yarn v2 and v3 users
If you're using Yarn v2 or v3, you may run into issues where Storybook can't resolve style-loader
or css-loader
. For example, you might get errors like:
Module not found: Error: Can't resolve 'css-loader'
Module not found: Error: Can't resolve 'style-loader'
This is because those versions of Yarn have different package resolution rules than Yarn v1.x. If this is the case for you, just install the package directly.
FAQ
Stories for pages/components which fetch data
Next.js page files can contain imports to modules meant to run in a node environment (for use in data fetching functions). If you import from a Next.js page file containing those node module imports in your stories, your Storybook's Webpack will crash because those modules will not run in a browser. To get around this, you can extract the component in your page file into a separate file and import that pure component in your stories. Or, if that's not feasible for some reason, you can polyfill those modules in your Storybook's webpackFinal
configuration.
Before
import fs from 'fs';
export default function Page(props) {
return;
}
export const getStaticProps = async () => {
};
After
import fs from 'fs';
import MyPage from 'components/MyPage';
export default function Page(props) {
return <MyPage {...props} />;
}
export const getStaticProps = async () => {
};
Starting with Next.js 13, you can also fetch data directly within server components in the app
directory. This does not (currently) work within Storybook for similar reasons as above. It can be worked around similarly as well, by extracting a pure component to a separate file and importing that component in your stories.
Before
async function getData() {
const res = await fetch(...);
}
export default async function Page() {
const data = await getData();
return
}
After
import MyPage from './components/MyPage';
async function getData() {
const res = await fetch(...);
}
export default async function Page() {
const data = await getData();
return <MyPage {...data} />;
}
Statically imported images won't load
Make sure you are treating image imports the same way you treat them when using next/image
in normal development.
Before using this framework, image imports just imported the raw path to the image (e.g. 'static/media/stories/assets/logo.svg'
). Now image imports work the "Next.js way", meaning that you now get an object when importing an image. For example:
{
"src": "static/media/stories/assets/logo.svg",
"height": 48,
"width": 48,
"blurDataURL": "static/media/stories/assets/logo.svg"
}
Therefore, if something in storybook isn't showing the image properly, make sure you expect the object to be returned from an import instead of just the asset path.
See local images for more detail on how Next.js treats static image imports.
Module not found: Error: Can't resolve [package name]
You might get this if you're using Yarn v2 or v3. See Notes for Yarn v2 and v3 users for more details.
Acknowledgements
This framework borrows heavily from these Storybook addons: