How to start Coding up your Design System in Vue
Tags: vue, design, webdev, tutorial
Published: 2020-01-31 19:23:24

Imagine this, you create your web app for this new epic idea and you implement the core functionality. The design can come later you think.

Well later is now, you're gaining traction and getting feedback, the time has come to overhaul or completely recreate your design!

In this article I'll go over the tools and methods you can use to achieve just that.

I personally fell into this situation with my web app Mylo, a workout management application. As it grew and came into contact with users, issues arose with color contrast, inconsistencies, and more.

Fixing these issues can be described in two pillars:

* Design System (how everything will look)
* Implementation (how to apply the look)

(I focus on look here but understand that design is only about 20% look, the rest must be considerations about the user experience)

Going through this has taught me a lot but also made obvious how much more there is to learn. If you find any mistakes or have some tips please let me know :D

I'll be using [TailwindCSS](https://tailwindcss.com/) in this article but everything applies just as well without.

## Table Of Contents

* [Directory Structure](#directory-structure)
* [Colors](#colors)
* [Icons](#icons)
* [Buttons](#buttons)
* [Text Inputs](#text-inputs)
* [Radio Buttons](#radio-buttons)
* [Select Input](#select-input)
* [Conclusion](#conclusion)


## Pillar 1: Design System

The first step, which is all the rage these days, is having a design system. A design system is essentially a visual codebase of your software, and just like a codebase, it is a complete and specific description of what the application should look like under almost any circumstance.

And so, the more you look into what makes up a design system, the more it feels like an impossible task. A complete design system involves the colors, spacing rules, text styles, buttons, containers, branding, accessibility, and so much more.

The best and most exhaustive resource I've found is the [Design System Checklist](https://designsystemchecklist.com/).

Therefore I feel more comfortable referring to what I have as a design library with loose guidelines. It works out because I can just refer to myself for any design questions ;)

So we'll be going over how to implement elements like buttons, icons, colors and a few input types.

## Pillar 2: Implementation

### Directory Structure <a name="directory-structure"></a>

Coding up design elements is amazing for reusability and consistency but it isn't very useful if components are all across the app in random, hard to access locations. We want them organized.

I recommend putting the smallest/atomic elements into the `src/components` folder and then into their own subfolders (icons, inputs, buttons, etc.)
Compound components, built out of the smaller ones, can be placed into the `src/layouts` folder, again with their own subfolders.

### Colors <a name="colors"></a>

Defining and enforcing colors is a good first step.

You generally have a clear discrete set of colors that your app allows, to bring them in you can either modify the TailwindCSS config file or add them as CSS variables.

Here is how it looks in TailwindCSS, I overwrote the default colors to enforce the use of the system colors but you can also extend the default theme and add your own colors.

```js
// tailwind.config.js

module.exports = {
  theme: {
    colors: {
      white: '#FFFFFF',
      black: '#000000',
      gray: {
        default: '#EEEEEE',
        dark: '#5D5D5C',
        darker: '#414040'
      },
      green: {
        default: '#83E8BC',
        dark: '#56806C',
        darker: '#445F51'
      },
      red: {
        default: '#F25F5C',
        dark: '#C15450',
        darker: '#8B4440'
      }
    }
  }
}
```

[TailwindCSS Docs - Theme Configuration](https://tailwindcss.com/docs/theme/)

If you're not using tailwind this can also be achieved using css variables like so:
```css
/* src/App.vue */
:root {
  --white: '#FFFFFF';
  --black: '#000000';
  --gray: '#EEEEEE';
  --gray-dark: '#5D5D5C';
  --gray-darker: '#414040';
  ...
}
```

### Icons <a name="icons"></a>

First off I recommend using SVG icons because of how configurable they are. You can change their size without any quality loss, dynamically change their color, and their file size is generally smaller than an equivalent png/jpg.

Getting the actual SVGs can be done through the export options of design tools like Figma, Sketch, or Illustrator. 

Once you have the files you can further optimize them with SVGO, there is a command line tool and a web based one.

{% github svg/svgo no-readme %}

{% github jakearchibald/svgomg no-readme %}

Both work automatically by pointing them to the file though the web version makes the available options more accessible. Make sure to have a look at the final result to make sure your icons still look fine.

Then we bring the icons into our Vue app, I used a method recommended in the Vue Cookbook. It's a system made by Sarah Drasner, the SVG queen, and you can find the link for it [here](https://vuejs.org/v2/cookbook/editable-svg-icons.html).

To make it work with TailwindCSS, you'll need to make a couple of changes:

```html
<template functional>
  <svg xmlns="http://www.w3.org/2000/svg"
    :width="props.size"
    :height="props.size"
    viewBox="0 0 16 16"
    :aria-labelledby="props.iconName"
    role="presentation"
    class="fill-current inline-block align-baseline"
    :class="[
      data.class,
      data.staticClass
    ]"
    style="margin-bottom: -2px;"
  >
    <title lang="en">{{ props.icon.replace(/-/g, ' ') }} icon</title>
    <component :is="injections.components[props.iconName]" />
  </svg>
</template>
```

Since SVGs themselves are rather light, it felt like a lot of overhead to use full components, so I made some further changes to make use of functional components, you can check out my fork here:

{% github MaxMonteil/vue-sample-svg-icons no-readme %}

### Buttons <a name="buttons"></a>

Initially I wanted to bring in buttons the same way as with icons, using Vue components but that ended up being deceptively complicated. The component had to work with buttons, links, or a `router-link` (using vue-router).

Supporting links was important for accessibility and semantics as links are meant to take you to another page whereas buttons should not.

As a solution I extracted the common classes into their own utilities in TailwindCSS, which in pure css is just a normal class rule.

Some examples:

```css
@tailwind base;
@tailwind components;

.btn {
    @apply font-medium rounded align-bottom;
}

.btn-primary {
    @apply px-8 py-2;
}

.btn-secondary {
    @apply px-5 py-1;
}

.btn-white {
    @apply text-green-darker bg-white;
}

.btn-green {
    @apply text-white bg-green-dark;
}

@tailwind utilities;
```

### Text Inputs <a name="text-inputs"></a>

For inputs we can use Vue components but there are a few things to take into consideration.

Our wrapper components need to be lightweight and transparent, we can do that by using functional components and attaching all attributes and event listeners.

I also took the chance to include the label into the component. It fits the design, is more accessible, and ensures I never forget them.

Start off with a `BaseInput.vue` component:

```html
<!-- src/components/inputs/BaseInput.vue -->
<template functional>
  <label
    :ref="data.ref"
    class="text-sm leading-none font-medium"
    :class="props.makeGray ? 'text-gray-darker' : 'text-green-darker'"
  >
    {{ props.label }}
    <input
      type="text"
      :ref="data.ref"
      class="block mt-2 bg-white rounded w-full outline-none focus:shadow"
      :class="[
        data.class,
        data.staticClass,
      ]"
      :style="[
        data.style,
        data.staticStyle,
      ]"
      v-bind="data.attrs"
      v-on="{ ...listeners, input: e => listeners.input(e.target.value) }"
    >
  </label>
</template>
```

And here is an example use of `BaseInput.vue`:
```html
<!-- src/components/inputs/InputLarge.vue -->

<template functional>
  <component
    :is="injections.components.BaseInput"
    :label="props.label"
    :makeGray="props.makeGray"
    class="font-medium text-3xl text-black pl-4 py-px"
    :class="props.makeGray ? 'bg-gray' : 'bg-white'"
    v-bind="data.attrs"
    v-on="listeners"
  />
</template>

<script>
import BaseInput from '@/components/inputs/BaseInput'

export default {
  name: 'inputLarge',
  inject: {
    components: {
      default: {
        BaseInput
      }
    }
  }
}
</script>
```

Notice how we call the `BaseInput.vue` component. Surprisingly, imported components are not exposed in functional components when using the template format. So instead we place the imported components into injections. They could also placed into props if you prefer.

This method was brought up in this github issue:

{% github https://github.com/vuejs/vue/issues/7492 no-readme %}

### Radio Buttons <a name="radio-buttons"></a>

After all the elements we've done so far, Radio Buttons are not too different. The difference is that styling them can be more involved. While it is possible to use standard buttons instead, I wanted to use the default radio buttons, again for semantics and accessibility.

The trick I found was to use the behavior of labels wrapping radio buttons.
The buttons by themselves are small and hard to touch/click but if you wrap them in a label, clicking anywhere on the label box will also select the radio button.
Using this, I styled radio buttons by actually making the label look as I wanted and hiding the radio buttons inside the label.

```html
<template functional>
  <div
    :ref="data.ref"
    :class="[
      data.class,
      data.staticClass,
    ]"
    :style="[
      data.style,
      data.staticStyle,
    ]"
  >
    <label
      v-for="label in props.labels"
      :key="label.value || label"
      class="relative px-3 py-1 rounded"
      :class="(label.value || label) === props.modelValue ? '...' : '...'"
    >
      <slot :label="label">
        {{ label.text || label }}
      </slot>
      <input
        :ref="data.ref"
        type="radio"
        class="sr-only"
        :value="label.value || label"
        v-on="{ ...listeners, input: e => listeners.input(e.target.value) }"
        :checked="(label.value || label) === props.modelValue"
      >
    </label>
  </div>
</template>
```

Be careful when hiding the radio button as it still needs to be visible to screen readers, tailwind offers a class for this, in standard css that looks like this:

```css
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border-width: 0;
}
```

As for the `v-slot` and all the ORs (||), I explain those in the next section about `select` inputs.

A really helpful resource that also goes over checkboxes:

[Smashing Magazine - Creating Custom Inputs in VueJS](https://www.smashingmagazine.com/2017/08/creating-custom-inputs-vue-js)

### Select Input <a name="select-input"></a>

The select component is a fun one to wrap both in terms of design and functionality.

Design wise it was surprising to discover how "hacky" it is to change the default downward arrow. There are a few ways to do it but the trick I went with is to remove the default style by setting `appearance: none;` and then bringing in my SVG of choice with the URL function of CSS.

To do something similar you will need to encode your SVG tag into a URL compatible string, I found this site to do just that:

[URL Encoder for SVG](https://yoksel.github.io/url-encoder/)

Then there are a few more positioning and spacing styles to place the icon where you want.

For functionality, the end user should retain control over how the drop-down values are displayed, the go to solution is to use scoped slots. With this method our component can support any array of values.

This is because the official Vue doc shows examples using a String Array and an Object Array to populate the select.

```html
<template functional>
  <label
    class="text-sm font-medium text-green-darker"
  >
    {{ props.label }}
    <select
      :ref="data.ref"
      class="custom-arrow bg-no-repeat block mt-2 pl-2 pr-6 bg-white rounded text-black text-lg outline-none focus:shadow"
      :class="[
        data.class,
        data.staticClass,
      ]"
      :style="[
        data.style,
        data.staticStyle,
      ]"
      v-bind="data.attrs"
      v-on="{ ...listeners, input: e => listeners.input(e.target.value) }"
    >
      <option disabled value="">-</option>
      <option
        v-for="option in props.values"
        :value="option.value || option"
        :key="option.value || option"
      >
        <slot :option="option" />
      </option>
    </select>
  </label>
</template>

<script>
export default {
  name: 'inputSelect',
  props: {
    label: {
      type: String,
      require: true
    },
    values: {
      type: [Array, Object],
      require: true
    }
  }
}
</script>

<style scoped>
.custom-arrow {
  -moz-appearance: none;
  -webkit-appearance: none;
  appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='10'%3E%3Cpath fill='%23000000' d='M7.443 9.057L5.229 6.843a.666.666 0 01.943-.942l2.214 2.214 2.199-2.199a.666.666 0 11.942.942l-3.142 3.143-.942-.944z'/%3E%3C/svg%3E");
  background-origin: border-box;
  background-position: top 35% right 0.5rem;
}
</style>
```

These wrapper components also offer the chance to enforce best practices like a label for an input and an empty disabled first option for better iOS support.

## Conclusion <a name="conclusion"></a>

Well you've reached the end, this covers all the components I brought in from my design library. It really just scratches the surface of what is possible and I couldn't hope to be fully exhaustive. Nonetheless, I hope this helped you out and encouraged you to tackle that redesign or even just start to design!

The next step after wrapping all these input fields would be to compose them into a form or other layout for reuse, these are some of the components you'd place into the `src/layouts` folder. You can expect a follow up article once I finish it up myself :D

Feel free to leave any comments below and if you have questions or want to follow up, you can find me on twitter [@MaxMonteil](https://twitter.com/maxmonteil)

* [Markus O. - Setting up Tailwind CSS with Vue.js](https://markus.oberlehner.net/blog/setting-up-tailwind-css-with-vue/)
* [Markus O. - Reusable Functional Vue.js Components with Tailwind CSS](https://markus.oberlehner.net/blog/reusable-functional-vue-components-with-tailwind-css/)