Recently I started to re-design my website, I decided to use this as an opportunity to learn some new technologies such as Gatsby, Tailwind. I also decided to try using Storybook. For this said project I used MDX to create my Storybook stories. In this article, I will show you how you can create Storybooks stories, for a Gatsby project with TailwindCSS, Typescript using MDX.

You can find an example project using this here. You can also find a demo site for said project.

This article assumes you already familiar with Typescript, TailwindCSS and Gatsby.

Storybook

Storybook is an open source tool for developing UI components in isolation for React, Vue, and Angular. It makes building stunning UIs organized and efficient. - Storybook Website

Storybook allows us to create and test (visually) components in isolation. It can be a great way to both document all of your components but also speed up development as all you need to focus on is one component at a time. Storybook also has a ton of extra plugins/addons which can help to customise storybooks to your liking. One such example being checking for any accessibility issues your components may have.

MDX

MDX is a combination of markdown mixed with JSX. It allows us to “execute” and “render” JSX code from within an MDX document. When used with Storybook it means we get all of the flexibility of markdown. So we can use normal markdown syntax, to document our component. We also get access to MDX-flavored Component Story Format (CSF) which includes a collection of components called “Doc Blocks”, that allow Storybook to translate MDX files into storybook stories.

Setup

OK let’s go over what we need to do, first let’s create our gatsby site by using the gatsby-cli tool.

gatsby new gatsby-site
cd gatsby-site

TailwindCSS

Now let’s see how we add tailwindcss to this site:

yarn add gatsby-plugin-typescript gatsby-plugin-postcss tailwindcss twin.macro postcss-preset-env
vim gatsby-config.js
vim postcss.config.js
vim tailwind.config.js
mkdir -p src/styles/
vim src/styles/globals.css
vim gatsby-browser.js

We need to update the gatsby-config.js file to add support for both typescript and PostCSS. Tailwind is written in PostCSS so we need to include that in our gatsby file. You can either replace the default gatsby-config.js or update the plugins.

const plugins = ["gatsby-plugin-typescript", "gatsby-plugin-postcss"];

module.exports = {
  plugins,
};

Next we add a postcss.config.js file as per the Tailwind instructions found here.

const tailwindcss = require('tailwindcss');

module.exports = () => ({
  plugins: [tailwindcss],
});

Finally, we create a tailwind.config.js file. Here we can add new colours, overwrite existing colours and extend the configuration such as adding news fonts (Inter). This file will get merged with the default config by Tailwind.

module.exports = {
  theme: {
    themeVariants: ["dark"],
    extend: {
      colors: {
        blue: {
          100: "#EBF2FD",
          200: "#CDDFFA",
          300: "#AFCBF6",
          400: "#72A5F0",
          500: "#367EE9",
          600: "#3171D2",
          700: "#204C8C",
          800: "#183969",
          900: "#102646",
        },
        monochrome: {
          900: "#333",
          800: "#444",
          700: "#666",
          600: "#999",
          500: "#ddd",
          400: "#eee",
          300: "#f3f3f3",
          200: "#f8f8f8",
          100: "#fff",
        },
      },
      fontFamily: {
        header: ["Inter"],
      },
    },
  },
  variants: {},
};

Next, to add the Tailwind styles or our app we need to create a CSS file, you can call this file whatever you want, you just need to make sure it gets imported in such a place it can be used by any of your components.

@tailwind base;
@tailwind components;
@tailwind utilities;

One place we can import this is in the gatsby-brower.js file. It should be empty, add the import shown below. We will add babel later on in the app, which will allow us to use imports in the style we’ve just described. In this example, we will use the ~ to mean src.

import "~/styles/globals.css";

Typescript

Now let’s add typescript to our project:

yarn add --dev react-docgen-typescript react-docgen-typescript-loader ts-loader typescript
vim tsconfig.json

We will add some extra libraries that will be used by Storybooks to parse our Typescript components. Like all Typescript projects, we need to include a tsconfig.json file. Note we add the "paths" so we can have cleaner imports, this will be used alongside Babel.

{
  "compileOnSave": false,
  "compilerOptions": {
    "target": "es5",
    "module": "es6",
    "types": ["node"],
    "moduleResolution": "node",
    "esModuleInterop": true,
    "lib": ["dom", "es2015", "es2017"],
    "jsx": "react",
    "sourceMap": true,
    "strict": true,
    "resolveJsonModule": true,
    "noUnusedLocals": true,
    "noImplicitAny": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true,
    "allowSyntheticDefaultImports": true,
    "downlevelIteration": true,
    "baseUrl": "./",
    "paths": {
      "~/*": ["src/*"]
    }
  },
  "include": ["./src/**/*"],
  "exclude": ["node_modules", "plugins"]
}

Babel

yarn add --dev babel-plugin-module-resolver babel-preset-gatsby babel-preset-react-app @babel/compat-data \
@babel/core @babel/preset-env babel-loader

vim .babelrc

Gatsby automatically uses Babel, however, to customise babel we need to create our own .babelrc file. You can read more about it here. The main reason we want to use it is to allow use to have cleaner imports. So we can use ~ instead of src in imports. So we can do import "~/styles/globals.css"; instead of import "../../../styles/globals.css"'.

You can read more about it here, I wrote a previous article on this topic.

{
  "env": {},
  "plugins": [
    [
      "module-resolver",
      {
        "root": ["./src"],
        "alias": {
          "~": "./src"
        }
      }
    ]
  ],
  "presets": [
    [
      "babel-preset-gatsby",
      {
        "targets": {
          "browsers": [">0.25%", "not dead"]
        }
      }
    ]
  ]
}

Storybook

We will use the latest versions of Storybook (v6) so we can access the latest features. We will go over how we can use these features in the next article.

First remove any lines in your package.json that start with @storybook. In my case, I removed @storybook/addon-actions, @storybook/add-links, @storybook/addons and @storybook/react.

yarn add --dev @storybook/[email protected] @storybook/[email protected] \
@storybook/[email protected]  @storybook/[email protected] \
@storybook/[email protected] [email protected]

npx -p @storybook/cli sb init -f
vim .storybook/main.js
vim .storybook/preview.js
vim preview-head.html
vim webpack.config.js

Next, we will update the main.js file. This will tell Storybook where to look for the stories, in this case in the src folder any file called x.stories.mdx or x.stories.tsx.

module.exports = {
  stories: ["../src/**/*.stories.@(tsx|mdx)"],
  addons: [
    "@storybook/addon-essentials",
    "@storybook/addon-docs",
    "@storybook/preset-typescript",
  ],
};

Next, lets update the preview file. Here is typically you can define global parameters and decorators. Again will see more of this in the next article.

import React from "react";

import { action } from "@storybook/addon-actions";
import { configure } from "@storybook/react";

import "../src/styles/globals.css";
import "./main.css";

configure(require.context("../src", true, /\.stories\.mdx$/), module);

global.___loader = {
  enqueue: () => {},
  hovering: () => {},
};
global.__PATH_PREFIX__ = "";
window.___navigate = (pathname) => {
  action("NavigateTo:")(pathname);
};

If we want to use any custom fonts, such as google fonts or other styles within our Tailwind, we need to define them here.

<link
  href="https://fonts.googleapis.com/css2?family=Inter:wght@600,900&display=swap"
  rel="stylesheet"
/>

Storybook uses webpack, so if we want to add extra webpack options, we do that here. This allows us to use things like Babel and PostCSS loader.

module.exports = ({ config }) => {
  config.module.rules[0].use[0].loader = require.resolve("babel-loader");
  config.module.rules[0].use[0].options.presets = [
    require.resolve("@babel/preset-react"),
    require.resolve("@babel/preset-env"),
  ];

  config.module.rules.push({
    test: /\.(ts|tsx)$/,
    loader: require.resolve("babel-loader"),
    options: {
      presets: [["react-app", { flow: false, typescript: true }]],
      plugins: [],
    },
  });

  config.module.rules.push({
    test: /\.css$/,
    use: [
      {
        loader: "postcss-loader",
        options: {
          sourceMap: true,
          config: {
            path: "./.storybook/",
          },
        },
      },
    ],
  });

  return config;
};

Component

Finally, let’s create a component that we will create a story for. First, create a new folder at src/components/Logo. In that folder let’s create the following files:

Note the comments in the Props will be the comments shown in our story later, if you use the correct addons for Storybook. We will go over this in the next article.

import React from 'react';
import tw from 'twin.macro';

export interface Props {
  /** The colour of the opening and closing tags. */
  accent?: string;
  /** The colour of main text. */
  color?: string;
  /** The colour when you hover over the logo. */
  hoverColor?: string;
  /** The main text of the logo for example, your name. */
  text: string;
  /** The size of the main text  */
  size?: 'xs' | 'sm' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl';
}

const Logo = ({
  accent = 'black',
  color = 'black',
  hoverColor = 'blue-500',
  text,
  size = '2xl',
}: Props) => (
  <LogoContainer
    className={`hover:text-${hoverColor} text-${color} lg:text-${size}
    md:text-xl sm:text-md text-sm`}
  >
    <Tag className={`text-${accent}`} data-testid="OpeningTag">
      &lt;
    </Tag>
    {text}
    <Tag className={`text-${accent}`} data-testid="ClosingTag">
      /&gt;
    </Tag>
  </LogoContainer>
);

const LogoContainer = tw.div`cursor-pointer font-header font-black tracking-wide `;

const Tag = tw.span``;

export default Logo;

This index file makes it easier to import the component from other files. As we don’t have to do import {Logo} from "src/components/Logo/Logo.ts we can use import {Logo} from "src/components/Logo.

export { default as Logo } from './Logo';

Storybook

Now we have set everything up but do we create a story for our component. First, create a new file at src/components/Logo/Logo.stories.mdx. You could keep this in another folder like storybooks/ or keep it in the same folder as your component, it’s all personal preference. Some people will also have all unit tests in the same folder src/components/Logo/.

import { Meta, Story, Preview, Props } from "@storybook/addon-docs/blocks";

import Logo from "./Logo";

<Meta title="Logo" component={Logo} />

# Logo

## Accent

You can adjust the accent (tags) color by passing the `accent` prop.

<Preview>
  <Story name="Accent Colour">
    <Logo accent="gray-500" color="blue-500" text="Haseeb" />
    <Logo accent="gray-500" color="black" text="Haseeb" />
  </Story>
</Preview>

Add the following to your package.json to the “scripts” section. We need to pass it the NODE_ENV=test environment variable, else the Gatsby Babel plugin will complain.

"storybook": "NODE_ENV=test start-storybook -p 6006",
"build-storybook": "NODE_ENV=test build-storybook"

Now we can run our Storybook by running the following command:

yarn storybook

That’s it! We managed to get Storybook to work with Gatsby. Where Gatsby is using Tailwind, Babel and Typescript.

Appendix