Skip to content
Santi020k

Search

Match titles, tags, and descriptions. Arrow keys to move, Enter to open, Esc to close.

Open from the page (not while typing in a field): / · K or CtrlK

    Permalink to this article
    Blog Article
    typescript

    Storybook in Action with Next.js, Tailwind and TypeScript

    Set up Storybook in a Next.js project with Tailwind and TypeScript. Build isolated UI components, write stories, and improve design system collaboration.

    7 min read

    1,216

    Continuing from our previous post on optimizing development workflows, today we’ll explore the power of Visual Development Experience (VDE) with Storybook in Action, alongside Next.js. While our last discussion emphasized the importance of pre-commit systems in ensuring code quality and stability, this post dives into enhancing UI development through Storybook’s isolated component environment. Let’s unlock the potential of these tools to streamline development, foster collaboration, and maintain UI consistency across projects.

    Read the Previous Post: Implementing Husky for Next.js

    GitHub Repository

    Getting Started with Storybook

    To integrate Storybook into your Next.js project, simply execute the following command:

    terminal
    npx sb init

    This command initiates the installation of necessary dependencies, configuration setups, and file creations.

    Once installed, start Storybook to visualize and interact with your components in an isolated environment:

    terminal
    npm run storybook
    # or
    yarn storybook

    I recommend removing demo components and configuring your existing components with Storybook. In our case, as we are utilizing aliases for imports, they might pose compatibility issues. To address this, add the following configuration to Storybook:

    .storybook/main.ts

    .storybook/main.ts
    import type { StorybookConfig } from "@storybook/nextjs";
    import path from 'path'
     
    const config: StorybookConfig = {
      stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
      addons: [
        "@storybook/addon-onboarding",
        "@storybook/addon-links",
        "@storybook/addon-essentials",
        "@chromatic-com/storybook",
        "@storybook/addon-interactions",
      ],
      framework: {
        name: "@storybook/nextjs",
        options: {},
      },
      docs: {
        autodocs: "tag",
      },
      staticDirs: ["../public"],
      webpackFinal: async (config) => {
        if (!config.resolve) return config;
     
        config.resolve.alias = {
          ...config.resolve.alias,
          '@/': path.resolve(__dirname, '../src/'),
          '@/atoms': path.resolve(__dirname, '../src/components/atoms'),
          '@/molecules': path.resolve(__dirname, '../src/components/molecules'),
          '@/organisms': path.resolve(__dirname, '../src/components/organisms')
        };
     
        return config;
      }
    };
     
    export default config;

    Note: New typescript way to implement aliases is better, in future post we can move our alias config to that

    With this configuration, we resolve the issue seamlessly.

    Tailwind Integration

    Enabling Tailwind support is straightforward. Simply include the following line in the Storybook preview:

    .storybook/preview.ts

    .storybook/preview.ts
    import "../src/app/globals.css";

    Here’s an example:

    .storybook/preview.ts

    .storybook/preview.ts
    import type { Preview } from "@storybook/react";
     
    import "../src/app/globals.css";
     
    const preview: Preview = {
      parameters: {
        controls: {
          matchers: {
            color: /(background|color)$/i,
            date: /Date$/i,
          },
        },
      },
    };
    export default preview;

    Now, with Storybook installed in our project, let’s proceed with some adjustments.

    In many projects, root configurations can become overwhelming. Thus, I advocate for managing configurations within a dedicated folder. Although optional, I highly recommend moving the .storybook folder into config/.storybook.

    To achieve this, make the following modifications:

    Move main and preview from the root project to:

    Project Structure
    ├── /config
    | ├── /.storybook
    | | ├── /main.ts
    | | ├── /preview.ts

    Adjust the Storybook scripts in package.json:

    package.json

    package.json
    {
      "scripts": {
        "storybook": "storybook dev -p 6006 -c config/.storybook",
        "build-storybook": "storybook build -c config/.storybook"
      }
    }

    And update the Storybook main config:

    config/.storybook/main.ts

    config/.storybook/main.ts
    import type { StorybookConfig } from "@storybook/nextjs";
    import path from 'path'
     
    const rootDir = path.resolve(__dirname, '../..');
     
    const config: StorybookConfig = {
      stories: [`${rootDir}/**/*.mdx`, `${rootDir}/**/*.stories.@(js|jsx|mjs|ts|tsx)`],
      addons: [
        "@storybook/addon-onboarding",
        "@storybook/addon-links",
        "@storybook/addon-essentials",
        "@chromatic-com/storybook",
        "@storybook/addon-interactions",
      ],
      framework: {
        name: "@storybook/nextjs",
        options: {},
      },
      docs: {
        autodocs: "tag",
      },
      staticDirs: [`${rootDir}/public`],
      webpackFinal: async (config) => {
        if (!config.resolve) return config;
     
        config.resolve.alias = {
          ...config.resolve.alias,
          '@/': `${rootDir}/src/`,
          '@/atoms': `${rootDir}/src/components/atoms`,
          '@/molecules': `${rootDir}/src/components/molecules`,
          '@/organisms': `${rootDir}/src/components/organisms`
        };
     
        return config;
      }
    };
     
    export default config;

    config/.storybook/preview.ts

    config/.storybook/preview.ts
    import type { Preview, ReactRenderer } from "@storybook/react";
    import { withThemeByClassName } from '@storybook/addon-themes';
    import '@storybook/addon-console';
     
    import "../../src/app/globals.css";
     
    // Preview configurations...
     
    export default preview;

    Now, creating component stories is straightforward. Let’s use our button component as an example:

    src/components/atoms/button/button.stories.ts

    src/components/atoms/button/button.stories.ts
    import Button, { type ButtonProps, ButtonSizes, ButtonVariations } from './button'
     
    import type { Meta, StoryObj } from '@storybook/react'
    import { fn } from '@storybook/test'
     
    const meta = {
      title: 'Design System/Atoms/Button',
      component: Button,
      parameters: {
        layout: 'centered'
      },
      tags: ['autodocs'],
      args: { onClick: fn() },
      argTypes: {
        variation: {
          control: { type: 'select' },
          description: 'The variation of button',
          options: Object.values(ButtonVariations),
          table: {
            defaultValue: { summary: ButtonVariations.Primary },
            type: { summary: 'ButtonVariations' }
          }
        },    size: {
          control: { type: 'select' },
          description: 'The size of button',
          options: Object.values(ButtonSizes),
          table: {
            defaultValue: { summary: ButtonSizes.Medium },
            type: { summary: 'ButtonSizes' }
          }
        }
      }
    } satisfies Meta<typeof Button>
     
    export default meta
     
    type Story = StoryObj<ButtonProps>
     
    export const Primary: Story = {
      args: {
        label: 'Primary Button',
        variation: ButtonVariations.Primary,
        size: ButtonSizes.Medium
      }
    }
     
    export const Secondary: Story = {
      args: {
        label: 'Secondary Button',
        variation: ButtonVariations.Secondary
      }
    }
     
    export const Success: Story = {
      args: {
        label: 'Success Button',
        variation: ButtonVariations.Success
      }
    }
     
    export const Danger: Story = {
      args: {
        label: 'Danger Button',
        variation: ButtonVariations.Danger
      }
    }
     
    export const Small: Story = {
      args: {
        label: 'Primary Button',
        variation: ButtonVariations.Primary,
        size: ButtonSizes.Small
      }
    }
     
    export const Medium: Story = {
      args: {
        label: 'Primary Button',
        variation: ButtonVariations.Primary,
        size: ButtonSizes.Medium
      }
    }
     
    export const Large: Story = {
      args: {
        label: 'Primary Button',
        variation: ButtonVariations.Primary,
        size: ButtonSizes.Large
      }
    }

    In the repository, you’ll find two additional examples, covering both Vitest and stories.

    The end result will resemble this:

    Storybook

    Enhancing Your Storybook with Integrations

    To install these integrations, run the following command using npm or yarn:

    terminal
    npm install --save-dev \
      @storybook/addon-themes \
      @storybook/addon-storysource \
      @storybook/addon-console \
      @storybook/addon-actions \
      @whitespace/storybook-addon-html \
      storybook-addon-pseudo-states

    or

    terminal
    yarn add -D @storybook/addon-themes @storybook/addon-storysource @storybook/addon-console @storybook/addon-actions @whitespace/storybook-addon-html storybook-addon-pseudo-states

    Note

    While these are recommended integrations, feel free to explore and install additional ones based on your project requirements. You can find more integrations on the Storybook Integrations page.

    Once installed, update your Storybook configuration files as follows:

    config/.storybook/main.ts

    config/.storybook/main.ts
    import type { StorybookConfig } from "@storybook/nextjs";
    import path from 'path'
     
    import '@storybook/addon-console';
     
    const rootDir = path.resolve(__dirname, '../..');
     
    const config: StorybookConfig = {
      stories: [`${rootDir}/**/*.mdx`, `${rootDir}/**/*.stories.@(js|jsx|mjs|ts|tsx)`],
      addons: [
        "@storybook/addon-onboarding",
        "@storybook/addon-links",
        "@storybook/addon-essentials",
        "@chromatic-com/storybook",
        "@storybook/addon-interactions",
        "@storybook/addon-themes",
        "@storybook/addon-storysource",
        "@whitespace/storybook-addon-html",
        "storybook-addon-pseudo-states"
      ],
      framework: {
        name: "@storybook/nextjs",
        options: {},
      },
      docs: {
        autodocs: "tag",
      },
      staticDirs: [`${rootDir}/public`],
      webpackFinal: async (config) => {
        if (!config.resolve) return config;
        config.resolve.alias = {
          ...config.resolve.alias,
          '@/': `${rootDir}/src/`,
          '@/atoms': `${rootDir}/src/components/atoms`,
          '@/molecules': `${rootDir}/src/components/molecules`,
          '@/organisms': `${rootDir}/src/components/organisms`
        };
        return config;
      }
    };
    export default config;

    config/.storybook/preview.ts

    config/.storybook/preview.ts
    import type { Preview, ReactRenderer } from "@storybook/react";
    import { withThemeByClassName } from '@storybook/addon-themes';
    import '@storybook/addon-console';
     
    import "../../src/app/globals.css";
     
    const preview: Preview = {
      parameters: {
        controls: {
          matchers: {
            color: /(background|color)$/i,
            date: /Date$/i,
          },
        },
      },
      decorators: [
        withThemeByClassName<ReactRenderer>({
          themes: {
            light: 'light',
            dark: 'dark',
          },
          defaultTheme: 'light',
        }),
      ]
    };
     
    export default preview;

    Conclusions

    By integrating these powerful Storybook with this addons, you can elevate your development workflow, streamline component testing, and enhance collaboration within your team. Experiment with different integrations to tailor Storybook to your project’s unique needs, fostering a more efficient and productive development environment.

    Before integrating Storybook, consider ongoing maintenance. It’s crucial for updated documentation and avoiding issues. Regular upkeep includes component updates, testing, and dependency compatibility checks. Evaluate benefits against maintenance effort based on project stage and resources.

    Share this piece
    Keep reading

    More writing in the same thread.

    A few more posts that overlap in topic, tooling, or the engineering tradeoffs behind this article.

    Boosting Code Quality and Efficiency with My ESLint Configuration Library
    Writing Case Study 5 min read

    Boosting Code Quality and Efficiency with My ESLint Configuration Library

    Reusable ESLint library for React, Next.js, and TypeScript projects. Enforces code quality with flat config support and simplifies the move from ESLint 8 to 9.

    In series: ESLint in Practice · Part 2

    Get new posts in your inbox

    Low volume — engineering notes, architecture writeups, and occasional career updates. Prefer a feed? Subscribe via RSS .

    We never share your email. Privacy policy .