Saltar al contenido
Saltar al texto del artículo

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.

Escrito por

Publicado
Lectura
8 min de lectura
Cantidad de palabras
1,527 palabras

Artículo en inglés

En esta página 5 secciones

Tutorial

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.

Getting Started with Storybook

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

bashterminal
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:

bash
npm run 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

typescript.storybook/main.ts
Ver el ejemplo completoContraer ejemplo36 líneas · Se copia el ejemplo completo
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;
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",
  ],

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

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

Here’s an example:

.storybook/preview.ts

typescript.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:

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

Adjust the Storybook scripts in package.json:

package.json

jsonpackage.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

typescriptconfig/.storybook/main.ts
Ver el ejemplo completoContraer ejemplo38 líneas · Se copia el ejemplo completo
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;
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",

config/.storybook/preview.ts

typescriptconfig/.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

typescriptsrc/components/atoms/button/button.stories.ts
Ver el ejemplo completoContraer ejemplo90 líneas · Se copia el ejemplo completo
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
  }
}
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'],

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

The end result will resemble this:

Storybook Button documentation showing a Primary Button preview with controls for onClick, label, variation, and size
Figure 1

One component. Every variation.

Enhancing Your Storybook with Integrations

To install these integrations, choose your package manager:

bash
npm install --save-dev @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 Integrations1 page.

Once installed, update your Storybook configuration files as follows:

config/.storybook/main.ts

typescriptconfig/.storybook/main.ts
Ver el ejemplo completoContraer ejemplo41 líneas · Se copia el ejemplo completo
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;
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",

config/.storybook/preview.ts

typescriptconfig/.storybook/preview.ts
Ver el ejemplo completoContraer ejemplo27 líneas · Se copia el ejemplo completo
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;
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,

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.

Continuar la serieArtículo 4 de 8

Building a Production Next.js App

A practical sequence for turning a fresh Next.js codebase into a product teams can lint, test, document, deploy, and secure with confidence.

Los 8 capítulos

Compartir

Compártelo.

Comentarios

Súmate a la conversación.

Lee las respuestas, haz una pregunta o comparte otra perspectiva.

Abrir conversaciónOcultar conversación

References

  1. Storybook Integrationsstorybook.js.org/integrations/

Storybook in Action with Next.js, Tailwind and TypeScripthttps://santi020k.com/es/blog/storybook-in-action-with-next-js-tailwind-and-typescript/