Skip to content

Fix: Storybook Not Working — Addon Conflicts, Component Not Rendering, or Build Fails After Upgrade

FixDevs · (Updated: )

Part of:  React & Frontend Errors

Quick Answer

How to fix Storybook issues — CSF3 story format, addon configuration, webpack vs Vite builder, decorator setup, args not updating component, and Storybook 8 migration problems.

The Problem

Storybook starts but a component doesn’t render — just a blank canvas:

// Button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';

const meta: Meta<typeof Button> = {
  component: Button,
};
export default meta;

export const Primary: StoryObj<typeof Button> = {};
// Canvas is blank — no error, just empty

Or args don’t update the component when controls are changed:

export const Configurable: StoryObj<typeof Button> = {
  args: {
    label: 'Click me',
    disabled: false,
  },
};
// Changing 'label' in the Controls panel doesn't update the rendered button

Or Storybook fails to build after upgrading:

npx storybook@latest upgrade
# Module not found: Error: Can't resolve '@storybook/addon-actions/dist/esm/preset'
# Error: Cannot find module 'storybook/internal/preview-api'

Or an addon conflicts with the Vite builder:

# info => Loading presets
# error => Error: addons-essentials: Cannot find module '@storybook/addon-links'

Why This Happens

Storybook 7 and 8 are fundamentally different from Storybook 6, and the most common breakage comes from leftover assumptions from older docs.

The first axis is the builder. Storybook now ships separate @storybook/react-vite, @storybook/react-webpack5, @storybook/nextjs, and similar framework packages. Older addons written against the Webpack 5 builder import internal paths that don’t exist in the Vite builder, which is why you sometimes see Cannot find module '@storybook/addon-actions/dist/esm/preset' despite the addon being installed. The fix is almost never “delete cache” — it is matching the framework package to your project bundler and pruning addons that haven’t been updated for the Vite path.

The second axis is version drift across @storybook/* packages. All of them must be on the same major (and ideally the same minor) version. npm and pnpm hoist transitive copies aggressively, so a single stale entry like @storybook/[email protected] next to [email protected] will break preset resolution with errors that look unrelated. npx storybook@latest upgrade and the bundled automigrate codemods fix the bulk of it.

The third axis is the story itself. A blank canvas with no error usually means the component renders nothing because required props aren’t being provided via args, the component reads from a context that isn’t wrapped by a decorator, or a global CSS import is missing from .storybook/preview.ts. Args not updating is almost always a component that hardcodes a value instead of consuming the prop Storybook is feeding it.

Diagnostic Timeline

Minute 0 — Blank canvas, no error. Your first instinct is to delete node_modules/.cache/storybook and restart. It almost never helps. Open the browser devtools and check the iframe at /iframe.html?id=your-story-id directly. If you see the story container but nothing inside, the component is rendering and returning null — a prop issue. If you see a hydration error, an addon decorator is wrapping the story with a provider that throws.

Minute 3 — Check the framework. Open .storybook/main.ts and look at framework.name. If it says @storybook/react (no -vite or -webpack5 suffix), you’re on an outdated config — Storybook 7+ requires the framework-builder combination. Switch to @storybook/react-vite for Vite projects or @storybook/react-webpack5 for CRA/webpack projects.

Minute 6 — Audit addon versions. Run npm ls @storybook/addon-essentials @storybook/addon-links storybook and look for mismatched majors. If you see 7.x and 8.x side by side, that’s the failure. Pin every @storybook/* package to the same major in package.json, delete node_modules plus the lockfile, and reinstall.

Minute 10 — Run automigrate. npx storybook@latest automigrate detects most addon-essentials conflicts, missing addon-onboarding, removed storyStoreV7 flags, and the CSF3 migration. Read the diff before accepting it — automigrate sometimes rewrites main.ts aggressively.

Minute 15 — Inspect args binding. If the component renders but controls don’t update it, open the story file and confirm the component destructures the prop name from its arguments. A <Button label="Click" /> that hardcodes Click inside the component will never react to the Controls panel no matter what args say.

Minute 20 — Check preview.ts CSS imports. If styles are missing, Storybook needs your global CSS imported explicitly in .storybook/preview.ts. Tailwind needs ./.storybook/**/* added to the content glob in tailwind.config.js, otherwise classes used only inside stories get tree-shaken.

Minute 30 — Reinstall from scratch. If automigrate didn’t converge, the cleanest reset is rm -rf node_modules package-lock.json and npx storybook@latest init --force on a fresh branch. Compare the generated .storybook/main.ts against yours to spot the divergence.

Fix 1: Write Stories Correctly with CSF3

Component Story Format 3 (CSF3) is the current standard:

// Button.tsx
interface ButtonProps {
  label: string;
  variant?: 'primary' | 'secondary';
  disabled?: boolean;
  onClick?: () => void;
}

export function Button({ label, variant = 'primary', disabled = false, onClick }: ButtonProps) {
  return (
    <button
      className={`btn btn-${variant}`}
      disabled={disabled}
      onClick={onClick}
    >
      {label}
    </button>
  );
}

// Button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';

// Meta — defines the component and default settings
const meta = {
  title: 'Components/Button',  // Story sidebar path (optional)
  component: Button,
  parameters: {
    layout: 'centered',  // 'centered' | 'fullscreen' | 'padded'
  },
  tags: ['autodocs'],  // Generate automatic documentation
  argTypes: {
    variant: {
      control: 'select',
      options: ['primary', 'secondary'],
    },
    onClick: { action: 'clicked' },  // Show action in Actions panel
  },
  args: {
    label: 'Button',  // Default args for ALL stories in this file
  },
} satisfies Meta<typeof Button>;

export default meta;
type Story = StoryObj<typeof meta>;

// Minimal story — uses default args
export const Primary: Story = {};

// Story with overrides
export const Secondary: Story = {
  args: {
    variant: 'secondary',
  },
};

// Story with all args
export const Disabled: Story = {
  args: {
    label: 'Disabled Button',
    disabled: true,
  },
};

// Story with render function for complex scenarios
export const WithCallback: Story = {
  args: {
    label: 'Click me',
  },
  render: (args) => (
    <div>
      <Button {...args} />
      <p>Click the button above</p>
    </div>
  ),
};

// Story with play function (interaction testing)
import { userEvent, within } from '@storybook/test';

export const Interactive: Story = {
  args: { label: 'Submit' },
  play: async ({ canvasElement, args }) => {
    const canvas = within(canvasElement);
    const button = canvas.getByRole('button', { name: 'Submit' });
    await userEvent.click(button);
    // args.onClick will have been called
  },
};

Fix 2: Fix Args Not Updating the Component

Components must use the props provided by Storybook’s args:

// WRONG — component doesn't use props, always shows 'Click me'
export function Button() {
  return <button>Click me</button>;
}

// ALSO WRONG — using hardcoded values instead of props
export function Button(props: ButtonProps) {
  const label = 'hardcoded';  // Ignoring props.label
  return <button>{label}</button>;
}

// CORRECT — always use the props passed in
export function Button({ label, variant = 'primary', onClick }: ButtonProps) {
  return (
    <button className={`btn-${variant}`} onClick={onClick}>
      {label}
    </button>
  );
}

// CORRECT — story provides args that are passed as props
export const Primary: Story = {
  args: {
    label: 'Submit',  // Passed as the 'label' prop
    variant: 'primary',
  },
};

Context-dependent components — use decorators:

// Component that needs a provider
export function UserAvatar() {
  const { user } = useAuth();  // Uses React context
  return <img src={user.avatar} alt={user.name} />;
}

// Story — wrap with the required provider
export const LoggedIn: Story = {
  decorators: [
    (Story) => (
      <AuthProvider value={{ user: { name: 'Alice', avatar: '/alice.jpg' } }}>
        <Story />
      </AuthProvider>
    ),
  ],
};

// Or set global decorators in .storybook/preview.ts
// .storybook/preview.ts
import type { Preview } from '@storybook/react';
import { ThemeProvider } from '../src/ThemeProvider';

const preview: Preview = {
  decorators: [
    (Story) => (
      <ThemeProvider theme="light">
        <Story />
      </ThemeProvider>
    ),
  ],
  parameters: {
    controls: { matchers: { color: /(background|color)$/i } },
  },
};

export default preview;

Fix 3: Fix Storybook After Upgrading

Upgrade all Storybook packages together — mixing versions causes errors:

# Upgrade everything to the latest version automatically
npx storybook@latest upgrade

# If the automatic upgrade fails, upgrade manually
# First, remove all @storybook/* packages
npm uninstall $(node -e "
  const pkg = require('./package.json');
  const deps = Object.keys({ ...pkg.dependencies, ...pkg.devDependencies });
  console.log(deps.filter(d => d.startsWith('@storybook') || d === 'storybook').join(' '));
")

# Reinstall with the target version
npx storybook@8 init  # Or: npx sb@8 init

# Fix addon compatibility issues
npx storybook@latest automigrate

Common Storybook 8 migration changes:

// .storybook/main.ts — Storybook 8 format
import type { StorybookConfig } from '@storybook/react-vite';

const config: StorybookConfig = {
  stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
  addons: [
    '@storybook/addon-links',
    '@storybook/addon-essentials',
    '@storybook/addon-onboarding',
    '@storybook/addon-interactions',
    // Addon packages must match the installed Storybook version
  ],
  framework: {
    name: '@storybook/react-vite',  // Or @storybook/react-webpack5
    options: {},
  },
};

export default config;

Fix package version mismatches:

# Check all @storybook packages and their versions
npm ls | grep @storybook

# They should all be on the same major version (e.g., 8.x.x)
# If you see mixed versions, reinstall

# package.json — pin all to the same version
{
  "devDependencies": {
    "@storybook/addon-essentials": "^8.0.0",
    "@storybook/addon-interactions": "^8.0.0",
    "@storybook/addon-links": "^8.0.0",
    "@storybook/react": "^8.0.0",
    "@storybook/react-vite": "^8.0.0",
    "@storybook/test": "^8.0.0",
    "storybook": "^8.0.0"
  }
}

Fix 4: Configure Vite and Webpack Builders

// .storybook/main.ts — Vite builder configuration
import type { StorybookConfig } from '@storybook/react-vite';
import { mergeConfig } from 'vite';

const config: StorybookConfig = {
  framework: {
    name: '@storybook/react-vite',
    options: {},
  },
  viteFinalConfig: async (config) => {
    // Merge with your Vite config
    return mergeConfig(config, {
      resolve: {
        alias: {
          '@': '/src',  // Path aliases
        },
      },
      define: {
        'process.env.NODE_ENV': '"development"',
      },
    });
  },
};

// .storybook/main.ts — Webpack builder configuration
import type { StorybookConfig } from '@storybook/react-webpack5';

const config: StorybookConfig = {
  framework: {
    name: '@storybook/react-webpack5',
    options: {},
  },
  webpackFinal: async (config) => {
    // Add custom webpack rules
    config.module?.rules?.push({
      test: /\.scss$/,
      use: ['style-loader', 'css-loader', 'sass-loader'],
    });

    // Add path aliases
    if (config.resolve) {
      config.resolve.alias = {
        ...config.resolve.alias,
        '@': path.resolve(__dirname, '../src'),
      };
    }

    return config;
  },
};

Fix Tailwind CSS not applying in Storybook:

// .storybook/preview.ts
import '../src/index.css';  // Import your global CSS with Tailwind directives
// import '../src/styles/globals.css';  // Or wherever your Tailwind is

// Ensure Tailwind processes Storybook files
// tailwind.config.js
module.exports = {
  content: [
    './src/**/*.{js,jsx,ts,tsx}',
    './.storybook/**/*.{js,jsx,ts,tsx}',  // Include Storybook files
  ],
  // ...
};

Fix 5: Mock Modules and External Dependencies

// .storybook/preview.ts — mock modules globally
import { fn } from '@storybook/test';

// Mock a module for all stories
const preview: Preview = {
  parameters: {
    // Mock fetch/API calls
    mockAddonConfig: { disable: false },
  },
};

// Mock in individual stories
import * as RouterModule from 'react-router-dom';

export const WithNavigation: Story = {
  parameters: {
    // Storybook Router addon
    reactRouter: {
      routePath: '/users/:id',
      routeParams: { id: '1' },
    },
  },
};

// Mock a hook
import { useRouter } from 'next/navigation';

jest.mock('next/navigation', () => ({
  useRouter: () => ({
    push: fn(),
    back: fn(),
    pathname: '/users',
  }),
}));

// Or use decorator pattern to inject mock context
export const WithMockedRouter: Story = {
  decorators: [
    (Story) => (
      <RouterContext.Provider value={{
        push: fn(),
        pathname: '/test',
        query: {},
        asPath: '/test',
      } as NextRouter}>
        <Story />
      </RouterContext.Provider>
    ),
  ],
};

Fix 6: Test Stories with play Functions

// Use play functions for interaction testing
import { userEvent, within, expect } from '@storybook/test';

export const LoginForm: Story = {
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);

    // Fill in the form
    await userEvent.type(
      canvas.getByLabelText('Email'),
      '[email protected]'
    );
    await userEvent.type(
      canvas.getByLabelText('Password'),
      'password123'
    );

    // Submit
    await userEvent.click(canvas.getByRole('button', { name: 'Sign In' }));

    // Assert the result
    await expect(
      canvas.getByText('Welcome back, Alice!')
    ).toBeInTheDocument();
  },
};

// Run interaction tests in CI
// package.json
{
  "scripts": {
    "test-storybook": "test-storybook --url http://localhost:6006",
    "storybook": "storybook dev -p 6006"
  }
}
# Install and run Storybook test runner
npm install --save-dev @storybook/test-runner

# Run all story interaction tests
npx test-storybook

# Run in CI (start Storybook first)
npx concurrently -k -s first -n "SB,TEST" -c "magenta,blue" \
  "npx storybook dev --port 6006 --quiet" \
  "npx wait-on tcp:6006 && npx test-storybook"

Still Not Working?

Component renders but styles are missing — Storybook has its own bundle separate from your app. CSS imports must be added to .storybook/preview.ts. Check: global CSS, CSS modules (usually work automatically), and third-party CSS libraries (may need to be imported explicitly in preview). For Tailwind, add ./.storybook/**/*.{js,ts} and ./src/**/*.{js,ts,jsx,tsx} to the content array — story-only utility classes get purged otherwise.

@storybook/test vs @testing-library/react conflicts — Storybook 8 ships its own @storybook/test which re-exports Jest/Testing Library APIs. If you import from both, you may get conflicting versions. Prefer @storybook/test for interactions inside stories, and keep @testing-library/react for Jest/Vitest unit tests.

MSW integration for API mocking in Storybook — use storybook-addon-msw or msw-storybook-addon to mock API calls in stories. Initialize the Service Worker in .storybook/preview.ts using the same worker.start() pattern as your app. Stories can then define handlers in their parameters.msw.handlers property.

Vite dev server hangs on first start after upgrade — Storybook’s Vite builder pre-bundles every story file the first time it boots, which can take 60+ seconds on a large project. If it appears stuck, give it three minutes before killing it. The next start uses the dep cache and finishes in seconds. If it still hangs, delete node_modules/.vite-storybook and let it rebuild once.

Story file matches glob but doesn’t appear in the sidebar — Storybook discovers stories via the stories array in .storybook/main.ts. Common gotchas: the glob doesn’t include the file extension you use (.mjs is not in the default), the file doesn’t have a default Meta export, or the file exports a non-story value at the top level that crashes the loader. Check the terminal for a “Failed to load story” warning.

Decorator renders but story body is empty — a global decorator in preview.ts that returns <Wrapper><Story /></Wrapper> only works if Story is rendered. If you instead return <Wrapper>{Story()}</Wrapper> (calling Story as a function), some versions of Storybook lose the decorator context. Always render <Story /> as a JSX element, not as a function call.

For related frontend testing issues, see Fix: MSW Not Working, Fix: Jest Setup File Not Working, Fix: Vite Failed to Resolve Import, and Fix: Playwright Component Testing Not Working.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles