# Build Your Own Component Library Plugin with Vue 3 and Rollup

Component libraries such as [Vuetify](https://vuetifyjs.com/) and [ElementUI](https://element-plus.org/), among others, provide excellent foundational building blocks that let developers create great-looking interfaces fairly quickly and painlessly. There comes a time, however, when you may need to depart from these off-the-shelf solutions in favor of your own common interface components that can be used across all of your projects. Whether you need more fine-grained visual customization or to build more complex or domain-specific reusable UI patterns, rolling out a custom component library may be a worthy investment.

> I’m so fucking tired of re-creating the same login page for every app

— Anonymous developer at the last start-up I was working at

In this short guide, we will create a Vue 3 component library that can be installed in your application as a Vue plugin.

## Our Objective

Let’s call our component library *MyUI*.

Our end goal is to be able to:

1. Include *MyUI* as a project dependency in `package.json`. We should also namespace it with `@my-org` to avoid conflicts because it’s just good practice:
    

```json
{
  "dependencies": {
    "@my-org/my-ui": "^0.1.0"
  }
}
```

1. Install it as a Vue plugin:
    

```jsx
import { createApp } from 'vue'
import App from './App.vue'
import MyUI from '@my-org/my-ui'

createApp(App)
  .use(MyUI)
  .mount('#app')
```

1. Drop our *MyUI* components into our application templates without any additional `import`'s or configurations
    

```jsx
<template>
	<my-button>Click Me</my-button> 
</template>
```

## Our Plan

Here are the steps we are going to take to achieve our objective:

* Setup our project and create our first component
    
* Export our component kit to be used as a plugin
    
* Bundle our project to be a distributable package
    
* Publish our package
    

## Project Setup

If you’ve worked with Vue 3 recently, you are likely already familiar with tools like [Vite](https://vitejs.dev/) that provide project scaffolding and other front-end build utilities. However, in our case, all we need is a minimal setup and a way to bundle our components to be used as a distributable plugin.

We will set up our project structure manually and later use [Rollup](https://rollupjs.org/) for bundling, which has a fairly simple configuration (in fact, at the time of this writing, Vite uses Rollup under the hood as its main bundling tool).

Here is what our project file structure will look like:

```plaintext
.gitignore
package.json
rollup.config.js
dist/
src/
  components/
    MyButton.vue
  index.js
```

* `/dist` will contain our built and minified distributable plugin files
    
* `/src/components` will contain our components (we’ll use `MyButton.vue` as an example)
    
* `/src/index.js` will export all of our components as a plugin installation function
    

<div data-node-type="callout">
<div data-node-type="callout-emoji">ℹ</div>
<div data-node-type="callout-text">I highly recommend you organize your components hierarchically as not all components are equal. Brad Frost’s <a target="_blank" rel="noopener noreferrer nofollow" href="https://bradfrost.com/blog/post/atomic-web-design/" style="pointer-events: none">Atomic Design</a> is a great read and he presents what I believe to be a very effective methodology for creating design systems.</div>
</div>

Firstly, before doing anything else, let’s make sure `.gitignore` includes `node_modules`. You may want to include any other files or directories you would like git to ignore.

```bash
$ echo 'node_modules' >> .gitignore
```

Next, our `package.json` file should include our project dependencies:

```json
{
  "name": "@my-org/my-ui",
  "version": "0.1.0",
  "private": false,
  "type": "module",
  "main": "dist/my-ui.min.cjs",
  "module": "dist/my-ui.min.mjs",
  "files": [
    "dist/*"
  ],
  "scripts": {
    "build": "rollup -c"
  },
  "peerDependencies": {
    "vue": "^3.3.4"
  },
  "devDependencies": {
	"vue": "^3.3.4",
    "rollup": "^3.25.1",
	"rollup-plugin-vue": "^6.0.0",
    "rollup-plugin-postcss": "^4.0.2",
	"rollup-plugin-peer-deps-external": "^2.2.4",
	"@rollup/plugin-terser": "^0.4.3"
  }
}
```

We will need 4 different Rollup plugins:

* `rollup-plugin-vue`: allows Rollup to process Vue single file components (SFCs)
    
* `rollup-plugin-postcss`: lets Rollup process `<style>` blocks in our SFCs using PostCSS (more on why we are using this solution later)
    
* `rollup-plugin-peer-deps-external`: externalizes `peerDependencies` as we expect the consumer of our library to include Vue in their project instead of us bundling it with ours
    
* `@rollup/plugin-terser` to minify our distributable code (optional)
    

Finally, we can install:

```bash
$ npm install
```

## Creating a Component

Let’s create our first simple Vue component `src/components/MyButton.vue`:

```jsx
<template>
  <button class="my-button">
    <slot>Submit</slot>
  </button>
</template>

<script>
export default {
  name: 'MyButton'
}
</script>

<style scoped>
.my-button {
  background-color: #001C30;
  color: #fff;
  border: none;
  border-radius: 5px;
  padding: 0.5rem 1rem;
  cursor: pointer;
}

button:is(:hover, :focus, :active) {
  background-color: #176B87;
}
</style>
```

We are using `scoped` in our styles so that they do not leak into, or conflict with, other application components.

## Exporting Our Component in a Plugin

Creating a component is easy enough, but now `MyButton.vue` is just sitting there as a lonely file. What we need to do next is export this component in a [plugin](https://vuejs.org/guide/reusability/plugins.html) so it can be registered on a Vue app instance.

Vue’s `app.use()` takes a function or an object with an `install()` method. An instance of a Vue app is then in turn supplied to this function allowing us to register application-wide components using `app.component()`.

In `src/index.js`:

```jsx
import MyButton from './components/MyButton.vue'

const components = {
  MyButton
}

export default app => {
  for (const prop in components) {
    if (components.hasOwnProperty(prop)) {
	  app.component(
	    components[prop].name,
		components[prop]
	  )
	}
  }
}
```

## Bundling Our Package

To bundle our package with Rollup, we first need to set up `rollup.config.js`:

```jsx
import vue from 'rollup-plugin-vue'
import postcss from 'rollup-plugin-postcss'
import peerDepsExternal from 'rollup-plugin-peer-deps-external'
import terser from '@rollup/plugin-terser'

export default [
  {
    input: 'src/index.js',
    output: [
      {
        format: 'esm',
        file: 'dist/my-ui.min.mjs'
      },
      {
        format: 'cjs',
        file: 'dist/my-ui.min.cjs'
      }
    ],
    plugins: [
      vue({
        preprocessStyles: true,
        template: {
          isProduction: true
        }
      }),
      postcss(),
      peerDepsExternal(),
	  terser()
    ]
  }
]
```

We will output in two formats: `esm` and `cjs`. Both of these files are specified in our `package.json` file under the `module` and `main` fields respectively.

We tell the Vue plugin to process the template in production mode which prevents Vue from emitting development-only code.

Then we just run the build script:

```bash
$ npm run build
```

Once the build script has completed, we should see the two output files under `dist/`.

### Why Do We Need to Process CSS with a Separate Plugin?

At the time of this writing, `rollup-plugin-vue` version 6.0.0 stopped “just working” with Vue SFCs, and Rollup would throw the following error during the build process.

```plaintext
[!] RollupError: Unexpected token (Note that you need plugins to import files that are not JavaScript)
```

It took quite a bit of searching and some hair-pulling to finally find a workable, even if not-so-ideal, solution. [PostCSS](https://postcss.org/) is a tool for transforming CSS with JavaScript and has a huge list of useful plugins, and it does a good job of extracting the `<style>` tags from the Vue SFCs while maintaining scope.

Perhaps future versions of `rollup-plugin-vue` will address the above issue, but until then, this seems to be the next best approach.

## Publishing Our Package

Publishing a package is pretty straightforward with the npm registry:

```bash
$ npm publish
```

You may need to either add a user with `npm adduser` if you do not already have your credentials in `.npmrc`, or `npm login`.

Note that `private` is set to `false` in our `package.json`. This will make the access scope public, which means anyone can see and use it. You may want to set it to `true` if you do not want the package available publicly.

Alternatively, you do not have to publish your project to any registry and instead access it directly via a git repository:

```json
{
  "dependencies": {
    "@my-org/my-ui": "git+https://github.com/path/to/repo.git"
  }
}
```

That’s it! Now you may include the *MyUI* package as a dependency in your Vue project and use it as you would any other plugin.

## Additional Considerations

I tried to keep this guide as basic and short as possible, but there are a few additional next steps you may want to consider:

* Themeing: adding a customizable color scheme. An easy if not somewhat simplistic solution would be to use [CSS Custom Properties](https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties) whose values you can set as part of the plugin installation process (`app.use()` takes a second *options* parameter)
    
* Manual Imports: instead of including your entire UI component collection as a plugin, you may want to white-list your components directory (under `files` in `package.json`) so that individual components can be imported instead. This is particularly useful if your collection grows to be very large and you want to avoid heavy bundles.
    
* Tree Shaking: with the right project setup and configuration, Rollup can eliminate dead code and automatically optimize your bundle size. This topic has a significantly large enough scope to warrant a separate post, which I may write about sometime in the future.
    
* Accessibility and Internationalization: both should be part of every project but are often forgotten.
