Skip to content

Instantly share code, notes, and snippets.

@justincy
Last active April 5, 2024 22:19
Show Gist options
  • Save justincy/b8805ae2b333ac98d5a3bd9f431e8f70 to your computer and use it in GitHub Desktop.
Save justincy/b8805ae2b333ac98d5a3bd9f431e8f70 to your computer and use it in GitHub Desktop.
Configure Storybook to work with Next.js, TypeScript, and CSS Modules

In addition to the Storybook for React setup, you'll also need to install these packages:

npm i -D @babel/core babel-loader css-loader style-loader
const path = require('path');
module.exports = {
stories: ['../stories/**/*.stories.js', '../stories/**/*.stories.tsx'],
addons: ['@storybook/addon-actions', '@storybook/addon-links'],
presets: [path.resolve(__dirname, './next-preset.js')]
};
const path = require('path');
module.exports = {
webpackFinal: async (baseConfig, options) => {
// Modify or replace config. Mutating the original reference object can cause unexpected bugs.
const { module = {} } = baseConfig;
const newConfig = {
...baseConfig,
module: {
...module,
rules: [...(module.rules || [])]
}
};
// TypeScript with Next.js
newConfig.module.rules.push({
test: /\.(ts|tsx)$/,
include: [
path.resolve(__dirname, '../components'),
path.resolve(__dirname, '../stories')
],
use: [
{
loader: 'babel-loader',
options: {
presets: ['next/babel'],
plugins: ['react-docgen']
}
}
]
});
newConfig.resolve.extensions.push('.ts', '.tsx');
//
// CSS Modules
// Many thanks to https://github.com/storybookjs/storybook/issues/6055#issuecomment-521046352
//
// First we prevent webpack from using Storybook CSS rules to process CSS modules
newConfig.module.rules.find(
rule => rule.test.toString() === '/\\.css$/'
).exclude = /\.module\.css$/;
// Then we tell webpack what to do with CSS modules
newConfig.module.rules.push({
test: /\.module\.css$/,
include: path.resolve(__dirname, '../components'),
use: [
'style-loader',
{
loader: 'css-loader',
options: {
importLoaders: 1,
modules: true
}
}
]
});
return newConfig;
}
};
// If you need global CSS, you can import it here and Storybook will automatically include it in all stories.
// You don't need this if you don't have any global CSS.
import '../src/styles.css';
@andymeek
Copy link

andymeek commented Dec 2, 2020

Thanks @justincy!

I'm experiencing some issues with Storybook not applying directives (e.g., @apply) correctly, however, it works with NextJS.

Do you have any ideas about this, and do they work for you?

@justincy
Copy link
Author

justincy commented Dec 2, 2020

@andymeek I don't use @apply. Are you using Tailwind? I got that working once with @apply by including postcss-loader.

use: [
        'style-loader',
        {
          loader: 'css-loader',
          options: {
            importLoaders: 1,
            modules: true
          }
        }, 'postcss-loader'
      ]

@njcaballero
Copy link

njcaballero commented Dec 10, 2020

I managed to get SCSS modules working with @storybook/preset-scss :

{
  name: '@storybook/preset-scss',
  options: {
    cssLoaderOptions: {
      modules: {
        auto: true
      }
    }
  }
},

Global style import also works in preview.js.

https://github.com/storybookjs/presets/tree/master/packages/preset-scss

EDIT: I added the auto option to css modules to prevent classes in my global files to be scoped.

This worked for me too! thanks @drskullster

@EyalPerry
Copy link

EyalPerry commented Dec 27, 2020

Below is the config I used to get css-modules to work with tailwind + scss + anything you put in postcss- the same as in the SPA via Next.js v10.
Note that I manually installed all of the below packages since I've had some PostCss warnings which indicated I am not passing in any config, which made me suspect PostCss7 was used somewhere below. If you experience the lack of tailwind styling in storybook- that's probably it.
Also note that sass-loader comes last in the loader array.

 const removeIndex = newConfig.module.rules.findIndex(
      (rule) => rule.test.toString() === "/\\.css$/"
    );

    if (removeIndex !== -1) {
      newConfig.module.rules.splice(removeIndex, 1);
    }

    newConfig.module.rules.push({
      test: /\.(s*)css$/,
      loaders: [
        "style-loader",
        {
          loader: "css-loader",
          options: {
            importLoaders: 1,
            modules: { auto: true },
          },
        },
        {
          loader: "postcss-loader",
          options: {
            postcssOptions: {
              config: path.resolve(__dirname, "..", "postcss.config.js"),
            },
          },
        },
        "sass-loader",
      ],
    });

This is my package.json:

"devDependencies": {
    "@storybook/react": "6.1.11",
    "css-loader": "5.0.1",
    "postcss": "8.2.1",
    "postcss-flexbugs-fixes": "5.0.2",
    "postcss-import": "14.0.0",
    "postcss-loader": "4.1.0",
    "postcss-preset-env": "6.7.0",
    "sass": "1.30.0",
    "sass-loader": "10.1.0",
    "style-loader": "2.0.0"
  }

As a result, I was able to use both tailwind and scss syntax in my css modules:

.disabled {
  @apply hover:shadow-none;

  @extend .non-interactive;
}

Make sure you import your global css files in preview.js, the same way your _app component would.

@canerorenn
Copy link

canerorenn commented Jan 30, 2021

Thanks @justincy

A config for NextJS Absolute Imports and Module path aliases

somewhere convenient in next-preset.js

baseConfig.resolve.alias = {
  ...baseConfig.resolve.alias,
  "@/components": path.resolve(__dirname, "../components"),
};

@batur
Copy link

batur commented Feb 28, 2021

Thanks @justincy

@joebartels
Copy link

joebartels commented Mar 31, 2021

What a fun time figuring this out. thanks everyone above for the information 🙏 🙇.
Unfortunately none of yall's solutions seemed to work for my setup, but they gave me the direction to figure it out 😄

Here are relevant pieces to my setup. Most notably typescript, nextjs, tailwind, postcss, sass, css modules

"dependencies": {
    "next": "10.1.2",
    "react": "17.0.2",
    "tailwindcss": "^2.0.4"
},
"devDependencies": {
    "@storybook/react": "^6.2.0",
    "css-loader": "^3.6.0",
    "postcss": "^8.2.9",
    "postcss-loader": "^4.2.0",
    "sass": "^1.32.6",
    "sass-loader": "^10.1.1",
    "style-loader": "^1.3.0",
    "typescript": "^4.2.3",
}
// .storybook/main.js
const path = require("path");

module.exports = {
    stories: ["../src/**/*.stories.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"],
    addons: [
        "@storybook/addon-links",
        "@storybook/addon-essentials",
    ],
    webpackFinal: async (config) => {
        config.module.rules.push({
            test: /\.(sc|sa|c)ss$/,
            use: [
                "style-loader",
                {
                    loader: "css-loader",
                    options: {
                        importLoaders: 1,
                        modules: {
                            auto: true,
                        },
                    },
                },
                {
                    loader: "postcss-loader",
                    options: {
                        // it "compiles" when I omit these options but I didn't confirm whether the correct postcss
                        // version is imported when omiting this.. So having it here helps me sleep at night
                        implementation: require("postcss"),
                        postcssOptions: {
                            config: path.resolve(__dirname, "..", "postcss.config.js"),
                        },
                    },
                },
                "sass-loader",
            ],
            include: path.resolve(__dirname, "..", "src"),
        });

        return config;
    },
};

Key reasons why the above worked:

  1. { modules: { auto: true } } is magical.
    • Most notably it only enables CSS modules on filenames that match /\.module(s)?\.\w+$/i which allows you to use same rule for regular css files (css files that are not imported as modules into jsx/tsx) and not have to manually exclude .module.scss files (as others have done above 👆)
  2. test: /\.(sc|sa|c)ss$/
    • this project uses both scss and css (and potentially sass) 😨
  3. include: path.resolve(__dirname, "..", "src"),
    • without this, some 3rd party .css imports get processed and break for w/e reason 🤷‍♂️
  4. The order of the loaders, style-loader -> css-loader -> postcss-loader -> sass-loader
    • is only way everything seemed to work together

And here are things that DID NOT work and my (possibly incorrect) understanding on why...

@storybook/addon-postcss

I added it because I got a deprecation warning that relying on implicit PostCSS loader is deprecated, but b/c I'm manually including the postcss-loader for .css files I don't think it's necessary and is just a distraction and not really doing anything.

@storybook/preset-scss

This was the most frustrating false hope. It's ONLY useful if you don't need to process s[ca]ss files with postcss. But because I'm using tailwindcss (which relies on postcss) it's completely useless. See why, here.

P.S. what was crucial to debugging this was printing the webpack storybook is using, via:

npm run storybook -- --debug-webpack

Cheers!

@skworden
Copy link

@joebartels - Thank you! - This is the only config I could get to work with SB v6.4, next 11, and SCSS.

@kimizuy
Copy link

kimizuy commented Oct 17, 2021

@justincy Thank you for your guide!

I made a minimal example using Next.js(v11) + Storybook(v6.3) + CSS Modules.
It works with .module.css and .module.scss.
with-storybook-css-modules-app

It is configured with webpack5 (note: storybook is experimental now).
I'm referring to @joebartels' configuration. Thanks!

// .storybook/main.js
const path = require('path')

module.exports = {
  stories: ['../stories/*.stories.@(ts|tsx|js|jsx|mdx)'],
  addons: ['@storybook/addon-links', '@storybook/addon-essentials'],
  webpackFinal: async (config) => {
    config.module.rules.push({
      test: /\.scss$/,
      use: [
        'style-loader',
        {
          loader: 'css-loader',
          options: {
            modules: {
              auto: true,
            },
          },
        },
        'sass-loader',
      ],
      include: path.resolve(__dirname, '../'),
    })
    return config
  },
  core: {
    builder: 'webpack5',
  },
}

@natterstefan
Copy link

Thank you @joebartels, I've one question left regarding your config. Does your config also support *.module.css files or only *.module.scss. I ran into several issues when trying to work with *.module.scss in my next-scss-tailwind-postcss setup when using modules that do not need to be .scss files necessarily.

FTR: In the end I renamed all my *.module.css files, but I wonder what's happening here. It worked without issues in next itself. 🤷

@tidusia
Copy link

tidusia commented Dec 7, 2021

For those who do not need Sass, I created an example repo with Next.js + Storybook + Tailwind + CSS Module support that does not require to modify the webpackFinal config.

Here is a git diff that focus on the setup.

@Pipe-Runner
Copy link

@tidusia Thanks, your config worked like a charm. I have one question though, why is babel-core and babel-loader needed as mentioned in your diff.

Also, I would like to point out, that if you are using path aliases via typescript, be sure to check this part of the documentation:
https://storybook.js.org/docs/riot/configure/webpack#typescript-module-resolution

@hieuvecto
Copy link

you saved my day

Copy link

ghost commented May 4, 2023

Amazing, thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment