Create a universal Vue component library with Vue Demi and Tailwind CSS
Tags: vue, library, tailwindcss, tutorial
Published: 2023-02-27 07:55:38


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

[Vue Demi](https://github.com/vueuse/vue-demi) is a developing utility that allows Vue developers to write a universal Vue library for Vue 2 and 3. Honestly though, it's just much better to migrate all your Vue libraries and applications to the latest version now that Vue 3 seems stable. But for the sake of flexibility for your existing Vue libraries, Vue Demi is the answer to your prayers.

For this tutorial, I also used Tailwind CSS to do our component design faster. [Tailwind CSS](https://tailwindcss.com/) is a utility-first CSS framework packed with a variety of classes that can be composed to build any design, directly in your markup. I know lots of people are hating this CSS framework (I was one of them at first), but it happened to grow on me.

Anyways, let's get started!

## Create base setup for the component library

Let's create the base setup with Vue 3 via [create-vue](https://github.com/vuejs/create-vue).

```
npm create vue@3
```

I selected these configurations, but you can change it based on your preferences.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4rla0ccmbi1c14957i5p.png)

After selecting configurations, go to the directory of the created project, then install the packages via `npm`.

```
cd vue-universal-library
npm install
```

## Add Tailwind CSS configuration

For this example, I will be using Tailwind CSS for the component design.

Let's start by installing Tailwind CSS and its peer dependencies to the created vue project. Then, run the init command to automatically create tailwind and postcss configuration files.

```
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
```

For reference on the later steps of this tutorial, here are the configurations inside my `tailwind.config.js` file.

```javascript
// tailwind.config.js

/** @type {import('tailwindcss').Config} */
module.exports = {
  content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
  theme: {
    extend: {
      colors: {
        primary: {
          DEFAULT: '#f25f70',
          light: '#f25f70e6',
          dark: '#c14c59'
        },
      }
    }
  },
  plugins: []
}
```

Lastly, you need to add the tailwind directives to your css file.

```css
/* tailwind.css */

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

Please refer to the [Tailwind CSS installation guide](https://tailwindcss.com/docs/guides/vite#vue) for more details.

## Install Vue Demi

We will now install [vue-demi](https://github.com/vueuse/vue-demi) to start with the creation of our universal component library.

```
npm i vue-demi
```

Update `package.json` to add `vue` and `@vue/composition-api` as peer dependencies.

```json
// package.son

{
  ...
  "dependencies": {
    "vue-demi": "^0.13.11"
  },
  "peerDependencies": {
    "@vue/composition-api": "^1.0.0-rc.1",
    "vue": "^2.0.0 || >=3.0.0"
  },
  "peerDependenciesMeta": {
    "@vue/composition-api": {
      "optional": true
    }
  },
  "devDependencies": {
    ...
    "vue": "^3.2.47",
    ...
  }
}
```

After this, we can now create our component but instead of importing vue-related stuff from `vue`, we import from `vue-demi`.

## Create your component

For this sample component library, I only created a button component based on Tailwind CSS design.

This is the snippet content of my [TwButton.ts](https://github.com/iambvterrcvp/vue-universal-library/blob/main/src/components/TwButton.ts) file which renders the button component based on `props` and checks Vue version via `isVue2` API of `vue-demi`. When doing version-specific logic, it is best to check the [migration guide](https://v3-migration.vuejs.org/) for the breaking changes between Vue 2 and Vue 3.

```typescript
// TwButton.ts

// import from 'vue-demi' instead of 'vue'
import { defineComponent, h, isVue2 } from 'vue-demi'

import '../assets/tailwind.css'

const TwButton = defineComponent({
  name: 'TwButton',
  props: { ... },
  emits: ['click'],
  setup(props, { emit, slots }) {
    function renderV2() { ... }

    function renderV3() {
      return () =>
        h(
          'button',
          {
            disabled: props.disabled,
            class: {
              'btn': true,
              'btn-flex': props.flex,
              'opacity-50 cursor-not-allowed': props.disabled,
              '!rounded-full': props.pill,
              'border': props.bordered,
              'border-dark': props.bordered && props.bg === 'clear',
              'border-primary-dark': props.bordered && props.bg === 'fill',
              'btn-clear': props.bg === 'clear',
              'btn-fill': props.bg === 'fill'
            },
            onClick: (event: Event) => emit('click', event)
          },
          [
            h('span', { class: 'btn-contents' }, [
              props.icon
                ? h(
                    'span',
                    {
                      class: {
                        'icon material-icons': true,
                        'ml-[-2px] mr-1.5': slots.default
                      }
                    },
                    props.icon
                  )
                : '',
              h('span', slots.default ? slots.default() : '')
            ])
          ]
        )
    }

    return isVue2 ? renderV2() : renderV3()
  }
})

export default TwButton
```

For this example, I just made a separate function to render the component based on Vue version, but you can make your own wrapper to the `h` function just like what this [vue-demi-related GitHub discussion](https://github.com/vueuse/vue-demi/discussions/74) provided.

## Build library and publish

Now that the component is created, let's build the library using [rollupjs](https://rollupjs.org/guide/en).

First, we update `package.json` to add `build:lib` script that removes `dist` folder then builds the library via rollup.

```json
// package.json

{
  "name": "vue-universal-library",
  ...
  "main": "dist/vue-universal-library.cjs.min.js",
  "browser": "dist/vue-universal-library.esm.min.js",
  "unpkg": "dist/vue-universal-library.global.min.js",
  "scripts": {
    ...
    "build:lib": "rimraf dist && rollup -c rollup.config.js --bundleConfigAsCjs",
    ...
  },
  ...
}

```

Then, we create the rollup configuration. You can check [here](https://github.com/iambvterrcvp/vue-universal-library/blob/main/rollup.config.js) for the full contents of `rollup.config.js`.

```javascript
// rollup.config.js

...
const pkg = require('./package.json')

export default {
  plugins: [...],
  external: ['vue-demi'], // add vue-demi as external
  input: ['src/index.ts'],
  output: [
    ...
    {
      format: 'umd',
      file: pkg.unpkg,
      sourcemap: true,
      name: 'VueUniversalLibrary',
      globals: {
        'vue-demi': 'VueDemi' // add vue-demi to output.globals
      }
    },
  ]
}
```

After implementing the rollup configuration, let's run the added build script. This will create `dist` folder that contains the built library files.

```
npm run build:lib
```

If all goes well, and assuming you already own an account, we can try publishing our library to `npm`.

```
npm publish
```

## Install library to Vue application

In either Vue 2 or Vue 3 application, let's install `vue-universal-library` via `yarn` or `npm`.

```
# using `yarn`
yarn add vue-universal-library

# using `npm`
npm install vue-universal-library
```

Once installed, let's import `vue-universal-library.min.css` to our main file.

```typescript
// main.ts

import 'vue-universal-library/dist/vue-universal-library.min.css'
```

Then, let's try using our button component inside `App.vue`.

```vue
<!-- App.vue -->

<script setup lang="ts">
...
import { TwButton } from 'vue-universal-library'

function click() {
  alert('test')
}
</script>

<template>
  <div id="app">
    ...

    <main>
      ...
      <tw-button bg="fill" icon="download" @click="click" pill bordered flex>button</tw-button>
    </main>
  </div>
</template>
```

In order to be able to render the `icon` props in our `TwButton` component, we need to install Material Icons just by adding this line to our `index.html`.

```html
<!-- index.html -->

<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
```

Also, if typescript returns a `Cannot find module 'vue-universal-library'...` error, we just need to create a declaration file for it such as below:

```typescript
// vue-universal-library.d.ts

declare module 'vue-universal-library'
```
 
After the setup, we can now run our Vue test application.

This is how our button looks like in Vue 2. (I did not change anything inside the pre-installed `App.vue` so all other displays are still there.)

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

I installed it in Vue 3 application with `bg=clear` and this is the result.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/94w32amli6hsjubosmbd.png)

So that's it! You can find the sample Vue universal component library full code repo [here](https://github.com/iambvterrcvp/vue-universal-library). Feel free to comment any suggestions or comments regarding `vue-demi` and this blog post. Have a Vue-tiful day! 😀