Creating a component library with Vite and Storybook

by Dennis — 5 minutes

Vite is the next generation frontend tool that aims to provide a faster and leaner development experience for modern web projects. It consists of a dev server that provides rich feature enhancements over native ES modules, and a build command that bundles your code with Rollup, pre-configured to output highly optimized static assets for production. This is the ideal solution for a monorepo component library with Storybook.

Bundlers

In the past I have created libraries with bundlers like Rollup, Webpack, TypeScript CLI and TSDX. These bundlers offer great solutions, but they also cause a lot of problems. For example, when using TypeScript, Storybook needs a way to understand the TypeScript code. This means Storybook requires the same configuration as your bundler. If you want to use Jest as well we also have to define this configuration for Jest. This means you have to configure your library in three different ways already.

Vite offers an out of the box configuration. The configuration is opinionated and comes with sensible defaults out of the box, but is also highly extensible via its Plugin API and JavaScript API with full typing support. It covers most modern setup like TypeScript, JSX, Vue, PostCSS, CSS Modules and even WebAssembly. Let’s get started to build our own library. For this example I’m using Lerna to create a monorepo.

First we can create our Vite project. I used Yarn for this example but npm works fine as well.

// Setup a project
yarn init

// Add Vite
yarn add -D vite

Now we can create our Storybook setup

npx sb@next init --builder storybook-builder-vite

Storybook also asks the environment you are building in. For now, let’s use a React library. This means I also have to install react and react-dom.

yarn add -D react react-dom

Now you can test storybook

yarn storybook

The first setup can take a little bit longer as Vite has no cache yet.

Example Storybook setup in Vite

Setup

Now we have Storybook running we can write our configuration. Let’s start with adding a tsconfig.json.

{
  "compilerOptions": {
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "isolatedModules": true,
    "jsx": "react-jsx",
    "lib": ["dom", "esnext", "dom.iterable"],
    "module": "esnext",
    "noEmit": true,
    "strict": true,
    "target": "esnext",
    "allowSyntheticDefaultImports": true
  },
  "include": ["./packages", "*.d.ts"],
  "exclude": ["node_modules", "dist"]
}

Let’s also add a Lerna setup

yarn add -D lerna
// lerna.json
{
  "npmClient": "yarn",
  "command": {
    "publish": {
      "allowBranch": "main",
      "conventionalCommits": true,
      "ignoreChanges": ["*.md", ".spec.ts", ".stories.ts"]
    }
  },
  "packages": ["packages/*"],
  "version": "independent"
}

Packages

And now our first package. Let’s create a button component for this example. First we create a new folder packages/button. In this folder we create a package.json and a vite.config.ts. The package.json should contain all information for npm.

{
  "name": "PACKAGE_NAME",
  "version": "0.0.0",
  "author": "YOU",
  "description": "PACKAGE_NAME DESCRIPTION",
  "main": "dist/PACKAGE_NAME.es.js",
  "module": "dist/PACKAGE_NAME.es.js",
  "scripts": {
    "build": "vite build",
    "build:watch": "vite build --watch"
  },
  "types": "./src/PACKAGE_NAME.d.ts"
}

Now we can create a vite.config.ts

import path from "path";

import { defineConfig } from "vite";
import dts from "vite-dts";

const isExternal = (id: string) => !id.startsWith(".") && !path.isAbsolute(id);

export default defineConfig(() => ({
  esbuild: {
    jsxInject: "import React from 'react'",
  },
  build: {
    lib: {
      entry: path.resolve(__dirname, "src/index.ts"),
      formats: ["es"],
    },
    rollupOptions: {
      external: isExternal,
    },
  },
  plugins: [dts()],
}));

I used vite-dts to generate types in my build output.

yarn add -D vite-dts

Now we can create our button component. Create an index.ts in the src folder. This is the input for our library.

// index.ts
export * from "./Button";

// Button.tsx
import type { ButtonHTMLAttributes } from "react";

export function Button({
  children,
  ...props
}: ButtonHTMLAttributes<HTMLButtonElement>) {
  return (
    <button type="button" {...props}>
      {children}
    </button>
  );
}

Now add a script to the root folder

"build": "lerna run build",
"build:watch": "lerna run build:watch",

Now run your script

yarn build

And we have an output :)

Stories

Now we have a working setup, we can extend our stories.

// .storybook/main.js
module.exports = {
  stories: ["../packages/**/*.stories.@(js|jsx|ts|tsx)"],
  addons: ["@storybook/addon-links", "@storybook/addon-essentials"],
  framework: "@storybook/react",
  core: {
    builder: "storybook-builder-vite",
  },
};

And add our first story in our button package

// packages/button/Button.stories.tsx
import type { ButtonHTMLAttributes } from "react";

import type { Meta, Story } from "@storybook/react";

import { Button as ButtonComponent } from "./Button";

export default {
  title: "Button",
} as Meta<ButtonHTMLAttributes<HTMLButtonElement>>;

const Template: Story<ButtonHTMLAttributes<HTMLButtonElement>> = (args) => (
  <ButtonComponent {...args}>Example button</ButtonComponent>
);

export const Button = Template.bind({});
Example Button in Storybook with Vite

Finishing touch

We now have a working setup. I'd personally like to add some extra features to it like eslint, prettier, Jest and GitHub pages. You can find a complete setup on my GitHub repo.

meerdivotion

Cases

Blogs

Event