Step-by-Step Guide: Creating a Template for Vue 3, Vuetify 3, and Storybook
Tags: vue, storybook, vuetify, typescript
Published: 2023-02-25 19:34:17

When working with Vue 3 and Vuetify 3, configuring Storybook 6 can be a challenge. In fact, it's very common to encounter compatibility issues and integration problems between these frameworks. This can be especially frustrating when trying to use configurations from your own project, such as implementing dark mode in to storybook's Vuetify. Fortunately, there are strategies you can use to simplify the process and streamline your workflow.

In this article, I'll walk you through a comprehensive step-by-step guide to creating a Vue 3 project, integrating it with Vuetify 3, and configuring Storybook on top.

---- 


## Initializing a Vue 3 project and integrate Vuetify 3 in to it.
Let's start by creating a new Vue 3 project and clean up the built in template inside it.

``` BASH
npm init vue@latest
```

Integrating Vuetify 3 in to the project
``` BASH
npm i vuetify
npm i @mdi/font
```
Create a new file in ``src/plugins/vuetify.ts``, include all the vuetify configurations there and then export it.

``` TYPESCRIPT
import { createVuetify } from "vuetify";
import type { ThemeDefinition } from "vuetify";
import "vuetify/styles";
import "@mdi/font/css/materialdesignicons.css";

import * as components from "vuetify/components";
import * as directives from "vuetify/directives";

const lightTheme: ThemeDefinition = {
  dark: false,
  colors: {
    primary: "#d35400",
    secondary: "#8e44ad",
    background: "#ecf0f1",
    error: "#c0392b",
    info: "#2980b9",
    success: "#27ae60",
    warning: "#f1c40f",
  },
};

const darkTheme: ThemeDefinition = {
  dark: true,
  colors: {
    primary: "#d35400",
    secondary: "#8e44ad",
    background: "#2f3640",
    error: "#c0392b",
    info: "#2980b9",
    success: "#27ae60",
    warning: "#f1c40f",
  },
};

export default createVuetify({
  components,
  directives,
  theme: {
    defaultTheme: "light",
    themes: {
      light: lightTheme,
      dark: darkTheme,
    },
  },
});
```
I have configured Light and Dark theme configurations as well. 

And finally import and use the configurations in ``main.ts``

```TYPESCRIPT
import { createApp } from "vue";
import App from "./App.vue";
import vuetify from "./plugins/vuetify";

createApp(App).use(vuetify).mount("#app");
```

## Installing and configuring storybook 
```BASH
npx storybook init
```
now storybook is installed and configured automatically within the project when the above script is done.

``stories`` and ``.storybook`` directories are generated in to the project as well.

``stories`` contains some basic components so that there are some examples to show when running ``npm run storybook``
You can delete this folder as we don't need it, we'll create a custom component manually.

``.storybook`` includes all the configurations for storybook

## Creating a custom component and preparing stories for it.

Our component will be a simple button named ``MyButton`` inside ``src/components`` directory that will have some basic props:

``` HTML
<script setup lang="ts">
import { withDefaults } from "vue";
export type Props = {
  /** Color of the button */
  color?: "primary" | "secondary" | "success" | "info" | "warning" | "error";

  /** To show a loading icon */
  loading?: boolean;

  /** the content of the button */
  content?: string;
};
withDefaults(defineProps<Props>(), {
  loading: false,
  color: "primary",
  content: "Button",
});
</script>

<template>
  <v-btn :color="color" :loading="loading">
    <!-- @slot To put any element inside the button-->
    <slot>
      {{ content }}
    </slot>
  </v-btn>
</template>
```
Note that the comments in the above example aren't just there for clarification purposes inside our code, but they are being displayed as descriptions for the props and slots by stories automatically.

Now according to our component we'll create a ``MyButton.stories.ts`` in the same directory of the component as below:

``` Typescript
import MyButton from "./MyButton.vue";

export default {
  title: "MyButton",
  component: MyButton,
  argTypes: {
    color: {
      control: {
        options: [
          "primary",
          "secondary",
          "success",
          "info",
          "warning",
          "error",
        ],
      },
    },
    loading: {
      control: {
        type: "boolean",
      },
    },
    content: {
      control: {
        type: "text",
      },
    },
  },
};

const Template = (args) => ({
  components: { MyButton },
  setup() {
    return { args };
  },
  template: '<MyButton v-bind="args" content="click me"/>',
});

export const Primary = Template.bind({});
Primary.args = {
  color: "primary",
};

export const Secondary = Template.bind({});
Secondary.args = {
  color: "secondary",
};

export const WithLoading = Template.bind({});
WithLoading.args = {
  color: "error",
  loading: true,
};
```
before running storybook we'll need to add some configurations to ``.storybook`` directory as well.


## Storybook configurations for vuetify

In order for vuetify to work inside storybook, some extra configurations are required.

First we need to have our own wrapper to run storybook on top it, for this purpose we'll create a file named ``storyWrapper.vue`` inside ``.storybook`` and it'll include all the HTML skeletons that are required for vuetify to run on it such as ``v-app``:

```HTML
<template>
  <v-app :theme="themeName">
    <v-main>
      <slot name="story"></slot>
    </v-main>
  </v-app>
</template>

<script>
export default {
  props: {
    themeName: String,
  },
};
</script>
``` 

Now we need to create a custom decorator for storybook so that it knows how to replace it's default wrapper with our own custom one, our decorator is named ``withVuetifyTheme.decorator.js`` inside ``.storybook`` folder.
Any custom configurations related to vuetify can be added in to this file, in our case it'll be only configuring themes.

Then we need to import and render the wrapper in an h function iside our decorator file.

``` TYPESCRIPT
import { h } from "vue";
import StoryWrapper from "./storyWrapper.vue";

export const DEFAULT_THEME = "light";

export const withVuetifyTheme = (storyFn, context) => {
  // Pull our global theme variable, fallback to DEFAULT_THEME
  const themeName = context.globals.theme || DEFAULT_THEME;
  const story = storyFn();

  return () => {
    return h(
      StoryWrapper,
      // give themeName to StoryWrapper as a prop
      { themeName },
      {
        story: () => h(story, { ...context.args }),
      }
    );
  };
};
```
Now that our decorator is done, we only have to import it in the ``preview.js`` file as below:

``` TYPESCRIPT
import { withVuetifyTheme } from "./withVuetifyTheme.decorator";
import { app } from "@storybook/vue3";
import vuetify from "../src/plugins/vuetify";

app.use(vuetify);

export const parameters = {
  actions: { argTypesRegex: "^on[A-Z].*" },
  controls: {
    expanded: true,
    sort: "requiredFirst",
  },
  docs: {
    inlineStories: false,
  },
};

export const globalTypes = {
  theme: {
    name: "vuetify.theme.defaultTheme",
    description: "Global theme for components",
    toolbar: {
      icon: "paintbrush",
      // Array of plain string values or MenuItem shape (see below)
      items: [
        { value: "light", title: "Light", left: "🌞" },
        { value: "dark", title: "Dark", left: "🌛" },
      ],
      // Change title based on selected value
      dynamicTitle: true,
    },
  },
};

export const decorators = [withVuetifyTheme];
```
Note that we have created a ``theme`` globalType to render a button on the toolbar of storybook in order to switch between themes for vuetify.

And now our storybook is configured with vuetify and is ready to be run:

```BASH
npm run storybook
```


![Image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2wcm8bj165c6zddhu9pa.png)

As it's clear on the toolbar you can as well switch between themes.

[**The repository for this project is available here**](https://github.com/peshanghiwa/vue3-vuetify3-storybook-template), you can directly download/use the template for your project.

Thanks for reading