TypeScript

Tailwind @apply not working in Storybook: add postcss-loader

Why a Sass CSS module ships a literal @apply line into Storybook with no error, why importing globals.css does not change it, and the one-loader fix, reproduced on webpack 5.

Mahdi Ben RhoumaMahdi Ben Rhouma
8 min read

TL;DR#

@apply is a Tailwind directive, and Tailwind runs as a PostCSS plugin. In a webpack-based Storybook, a rule such as use: ['style-loader', 'css-loader?modules&importLoaders', 'sass-loader'] sends your Sass modules through Sass and css-loader only. Sass keeps the unknown @apply line, css-loader keeps it too, and the story renders unstyled with no error. @storybook/addon-postcss does not help, because it only adds PostCSS to files that match .css. Add postcss-loader to the Sass rule, between css-loader and sass-loader, with a postcss.config.js that loads Tailwind. On Tailwind v4 with plain CSS modules, the symptom is a hard error instead, and the fix is a @reference line.

What the broken output looks like#

The Stack Overflow question uses Next.js CSS modules written in Sass, Storybook 6 with framework: '@storybook/react', and @storybook/addon-postcss configured with Tailwind. A custom webpackFinal rule handles the Sass files. Storybook started, but the components were unstyled wherever their module used @apply.

The same loader chain was rebuilt on webpack 5.112.0 with sass 1.106.1, css-loader 7.1.5, sass-loader 17.0.1 and Tailwind CSS 3.4.19 (Node 26, 22 October 2026). Take a Button.module.scss that uses a Sass variable and one @apply. Webpack reports compiled successfully, and this is the CSS it puts in the bundle:

text
webpack 5.112.0 compiled successfully in 1187 ms
.Xjc6sWQ7XO1Z01527y1P {
  border-radius: 0.5rem;
  @apply bg-blue-500 text-white font-bold py-2 px-4;
}

The Sass ran: $radius became 0.5rem, and css-loader hashed the class name. Tailwind never ran. The browser ignores the @apply line, so the button gets a radius and nothing else.

Why the Sass rule never reaches Tailwind#

Webpack applies the loaders of a rule in reverse order. The webpack loaders documentation puts it this way: "Loaders are evaluated/executed from right to left (or from bottom to top)." For ['style-loader', 'css-loader', 'sass-loader'], Sass compiles the file, css-loader resolves it into a module, and style-loader injects it. No step in that chain runs PostCSS, so the Tailwind plugin in your PostCSS config is never called for that file.

The addon in the question does not cover this rule. The source of @storybook/addon-postcss adds one rule whose default test is /\.css$/, with the loaders style-loader, css-loader and postcss-loader. A .scss or .sass file does not match that test, so it only goes through the custom rule from webpackFinal.

The default changed between major versions, which matters when you upgrade. Storybook's migration guide states: "Storybook 6.x installed postcss by default. In 7.0 built-in support has been removed for Webpack-based frameworks." The addon that replaced that default is no longer maintained either: its README opens with "This package is unmaintained and deprecated in favor of @storybook/addon-styling-webpack". On npm, its latest version is still 2.0.0, published in January 2021.

Fix: run PostCSS inside the Sass rule#

1. Add postcss-loader to the Sass rule#

Put postcss-loader between css-loader and sass-loader. Sass compiles first and turns the file into plain CSS. PostCSS then runs Tailwind on that CSS, and css-loader receives the result. Set importLoaders to 2, because the css-loader documentation defines it as how many loaders before css-loader "should be applied to @imported resources and CSS Modules/ICSS imports". Here those loaders are postcss-loader and sass-loader.

In the question's Storybook 6 main.js, keep the stories, addons and framework entries exactly as they are, including the @storybook/addon-postcss block that still compiles globals.css. Only the webpackFinal rule changes:

js
// .storybook/main.js (Storybook 6, @storybook/react) -- webpackFinal only
const path = require('path');
 
module.exports = {
  // stories, addons and framework: unchanged from your current file
  webpackFinal: async (config) => {
    config.module.rules.push({
      test: /\.s[ac]ss$/i,
      use: [
        'style-loader',
        { loader: 'css-loader', options: { modules: true, importLoaders: 2 } },
        'postcss-loader',
        'sass-loader',
      ],
      include: path.resolve(__dirname, '../'),
    });
    return config;
  },
};

The second answer to the question makes the same move, adding postcss-loader to its Sass rule. Its author was on Storybook 7 with @storybook/react-webpack5, not Storybook 6, and says the accepted answer did not work there.

2. Install a loader that matches your webpack, and a Tailwind config#

Which postcss-loader to install depends on the webpack version Storybook builds with. The Storybook 6 webpack builder docs state: "Storybook builds your project with Webpack 4 by default." The question's main.js sets no builder, so it is on webpack 4. The current postcss-loader line requires webpack 5: version 8.2.1 declares a peer dependency on webpack 5 only. Version 4.3.0 accepts webpack 4 or 5, and @storybook/addon-postcss 2.0.0 itself depends on the 4.x line. On a Storybook 6 project left on the default builder, pin that line. Pin Tailwind to v3 too, because a bare npm install tailwindcss now installs v4 (see the last section):

bash
# Storybook 6, default webpack 4 builder
npm install -D postcss-loader@^4 postcss tailwindcss@^3 autoprefixer

On Storybook 7 or later, or on Storybook 6 with the webpack 5 builder, the current postcss-loader is the right one:

bash
# webpack 5 builders
npm install -D postcss-loader postcss tailwindcss@^3 autoprefixer

The reproduction on this page used webpack 5 with postcss-loader 8.2.1. The webpack 4 combination was not run.

postcss-loader reads postcss.config.js from the project root. On Tailwind v3:

js
// postcss.config.js
module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
};

3. Storybook 10 with @storybook/react-webpack5#

The current release on npm is storybook 10.6.3. Its styling guide says that a webpack project using Sass needs extra configuration, and it recommends @storybook/addon-styling-webpack. That addon "takes Webpack module rules for your styling tools and replaces the existing rules in Storybook's Webpack config" (README).

The README has separate examples for PostCSS and Sass, and its Sass example has no postcss-loader. If you copy that example as it is, you get the same silent @apply problem. The entry below combines the two examples. Add it to the existing addons array of your .storybook/main.ts, next to your compiler addon and the rest, which stay as they are. Install @storybook/addon-styling-webpack and the loaders it names (style-loader, css-loader, postcss-loader, sass-loader and sass) as dev dependencies first:

ts
// .storybook/main.ts (Storybook 10, @storybook/react-webpack5) -- one entry of addons
const stylingAddon = {
  name: '@storybook/addon-styling-webpack',
  options: {
    rules: [
      {
        test: /\.s[ac]ss$/i,
        use: [
          'style-loader',
          { loader: 'css-loader', options: { modules: { auto: true }, importLoaders: 2 } },
          'postcss-loader',
          'sass-loader',
        ],
      },
    ],
  },
};
// then, in the config object: addons: [...yourExistingAddons, stylingAddon]

The loader chain in this rule is the one reproduced below. A full Storybook 10 build was not run for this page.

Verify the fix#

Make the single change from step 1 and rebuild. In the same reproduction, where importLoaders was 2 in both runs and only the postcss-loader entry differed, the module now holds real declarations:

text
webpack 5.112.0 compiled successfully in 1791 ms
.Xjc6sWQ7XO1Z01527y1P {
  border-radius: 0.5rem;
  --tw-bg-opacity: 1;
  background-color: rgb(59 130 246 / var(--tw-bg-opacity, 1));
  padding-top: 0.5rem;
  padding-bottom: 0.5rem;
  padding-left: 1rem;
  padding-right: 1rem;
  font-weight: 700;
  --tw-text-opacity: 1;
  color: rgb(255 255 255 / var(--tw-text-opacity, 1));
}

In Storybook, open the story and inspect the element. If the class rule still contains @apply, the file did not go through the rule you edited. If it contains background-color, Tailwind ran.

One email a month — the latest explained errors

What the site published that month — mostly Next.js, Supabase and Node errors, explained. You confirm by email; unsubscribe any time in one click.

Importing globals.css does not reach the modules#

The accepted answer adds import '../styles/globals.css' to .storybook/preview.js, where that file holds the three @tailwind directives. That import does give stories Tailwind's base styles and the utilities your content globs find. It does not change how Sass modules are compiled. In the reproduction, the entry imported globals.css (handled by a .css rule with postcss-loader) and the unchanged Sass rule. The global stylesheet compiled, and the module did not:

text
webpack 5.112.0 compiled successfully in 2022 ms
[global stylesheet: 10420 chars, contains "@apply": false]
.Xjc6sWQ7XO1Z01527y1P {
  border-radius: 0.5rem;
  @apply bg-blue-500 text-white font-bold py-2 px-4;
}

So keep the preview import if your stories use utility classes directly. The @apply lines inside modules still need postcss-loader.

When the error is loud instead of silent#

The rule tests .sass but your files are .scss. The question's rule uses test: /\.sass$/. A Button.module.scss does not match it, and webpack fails with a parse error rather than a missing style:

text
ERROR in ./src/Button.module.scss 1:12
Module parse failed: Identifier directly after number (1:12)
You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders

Use /\.s[ac]ss$/i to match both syntaxes.

Tailwind v4 and plain CSS modules. Tailwind v4 processes each CSS module on its own, and the compatibility guide says that "features like @apply won't work the way you expect unless you explicitly import your global styles as reference". With @tailwindcss/postcss 4.3.3, a Button.module.css containing @apply bg-blue-500 text-white font-bold; and no reference stops with:

text
Error: Cannot apply unknown utility class `bg-blue-500`. Are you using CSS modules or similar and missing `@reference`? https://tailwindcss.com/docs/functions-and-directives#reference-directive

Adding @reference "./app.css"; as the first line, where app.css contains @import "tailwindcss";, made the same file compile to background-color: var(--color-blue-500, oklch(62.3% 0.214 259.815));. The border-border page covers the other causes of this error, such as a v3 config that v4 no longer loads. Keep the v4 case out of Sass, though: the same guide says Tailwind v4 "is not designed to be used with CSS preprocessors like Sass, Less, or Stylus".

On @storybook/nextjs, let the framework build the rules#

The question predates Storybook's Next.js framework, whose first npm release came in October 2022. Its documentation says that with @storybook/nextjs, "Storybook will automatically handle the PostCSS config for you, including any custom PostCSS configuration". It also says that CSS, Sass and Scss modules "work as expected". The same page recommends @storybook/nextjs-vite for most Next.js projects. If you move to either framework, remove the hand-written Sass rule from webpackFinal rather than stacking it on top of the framework's own rules. This behaviour comes from Storybook's documentation; it was not reproduced here.

The next error after the fix#

If you add postcss-loader and then install Tailwind with a bare npm install tailwindcss, you get v4, and the v3-style tailwindcss: {} plugin entry fails with an error saying Tailwind has moved its PostCSS plugin to a separate package. The PostCSS plugin page explains that error. It is written for Vite, but the @tailwindcss/postcss part applies to webpack and Storybook too. If your postcss.config.js lists plugins as an array of plain strings, Storybook's postcss-loader throws before Tailwind runs; that case is covered in Invalid PostCSS Plugin found at plugins[0]. If the build is clean and the styles are still missing in a Next.js app, check the content globs as described in Tailwind not working with Next.js.

In every version of this problem, the question is which rule a file goes through, and whether that rule includes postcss-loader. Check that before you rewrite any @apply as utility classes.

Frequently Asked Questions

|

Have more questions? Contact us

Still stuck? Paste your error.

Paste the error message or stack trace — we check it against every fix and tool on the site.

Matching happens in your browser. When you search, a cleaned error signature is sent so we can spot errors we do not cover yet — never the raw text, your IP address or your e-mail. Do not paste passwords or keys. Details

Written by

Mahdi Ben Rhouma
Mahdi Ben Rhouma

Full-Stack Dev — Next.js & Supabase

Solo developer building SaaS products with Next.js and Supabase. Writing about production patterns the official docs skip.

Remote