Chapter 19 - Unlocking Vue.js: Build Stunning Design Systems with Storybook for Eye-Catching Interfaces

Vue.js and Storybook create robust design systems. Combine components like Button and Input for versatile interfaces. Use global styles for consistency. Document components in Storybook. Implement theme switching for advanced functionality.

Chapter 19 - Unlocking Vue.js: Build Stunning Design Systems with Storybook for Eye-Catching Interfaces

Vue.js and Storybook are a match made in heaven when it comes to creating a robust design system. I’ve been working with these tools for a while now, and I can confidently say they’re game-changers for front-end development.

Let’s dive into creating a Vue.js design system with Storybook. First things first, we need to set up our project. If you haven’t already, install Vue CLI globally:

npm install -g @vue/cli

Now, let’s create a new Vue project:

vue create vue-design-system
cd vue-design-system

With our Vue project set up, it’s time to add Storybook to the mix:

npx sb init

This command will install all the necessary dependencies and create a basic Storybook configuration for us. Once it’s done, you’ll see a new .storybook directory in your project root.

Now, let’s create our first component. We’ll start with a simple button component. Create a new file src/components/Button.vue:

<template>
  <button :class="['btn', `btn-${variant}`]" @click="onClick">
    <slot></slot>
  </button>
</template>

<script>
export default {
  name: 'Button',
  props: {
    variant: {
      type: String,
      default: 'primary',
      validator: (value) => ['primary', 'secondary', 'danger'].includes(value)
    }
  },
  methods: {
    onClick() {
      this.$emit('click')
    }
  }
}
</script>

<style scoped>
.btn {
  padding: 10px 20px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}
.btn-primary {
  background-color: #007bff;
  color: white;
}
.btn-secondary {
  background-color: #6c757d;
  color: white;
}
.btn-danger {
  background-color: #dc3545;
  color: white;
}
</style>

This button component is simple but versatile. It accepts a variant prop to change its appearance and emits a click event when clicked.

Now, let’s create a story for this component. In the src/stories directory, create a new file called Button.stories.js:

import Button from '../components/Button.vue';

export default {
  title: 'Components/Button',
  component: Button,
  argTypes: {
    variant: {
      control: { type: 'select', options: ['primary', 'secondary', 'danger'] }
    },
    default: { control: 'text' }
  },
};

const Template = (args, { argTypes }) => ({
  props: Object.keys(argTypes),
  components: { Button },
  template: '<Button v-bind="$props">{{ default }}</Button>',
});

export const Primary = Template.bind({});
Primary.args = {
  variant: 'primary',
  default: 'Primary Button',
};

export const Secondary = Template.bind({});
Secondary.args = {
  variant: 'secondary',
  default: 'Secondary Button',
};

export const Danger = Template.bind({});
Danger.args = {
  variant: 'danger',
  default: 'Danger Button',
};

This story file defines how our Button component will be displayed in Storybook. We’ve created three stories for our three button variants.

Now, let’s run Storybook and see our button in action:

npm run storybook

You should see Storybook open in your browser with your Button component displayed. You can interact with the component, change its props, and see how it behaves in different states. Pretty cool, right?

But a design system is more than just a collection of components. It’s about creating a cohesive visual language for your application. Let’s add some global styles to our design system.

Create a new file src/styles/variables.css:

:root {
  --color-primary: #007bff;
  --color-secondary: #6c757d;
  --color-danger: #dc3545;
  --font-family: 'Arial', sans-serif;
  --font-size-base: 16px;
  --spacing-unit: 8px;
}

Now, let’s use these variables in our Button component. Update the <style> section in Button.vue:

<style scoped>
.btn {
  font-family: var(--font-family);
  font-size: var(--font-size-base);
  padding: var(--spacing-unit) calc(var(--spacing-unit) * 2);
  border: none;
  border-radius: calc(var(--spacing-unit) / 2);
  cursor: pointer;
}
.btn-primary {
  background-color: var(--color-primary);
  color: white;
}
.btn-secondary {
  background-color: var(--color-secondary);
  color: white;
}
.btn-danger {
  background-color: var(--color-danger);
  color: white;
}
</style>

To make these variables available in Storybook, we need to update our Storybook configuration. Open .storybook/preview.js and add:

import '../src/styles/variables.css';

Now our Button component is using our global design tokens. This approach makes it easy to maintain a consistent look and feel across your entire application.

Let’s add another component to our design system. How about an Input component? Create a new file src/components/Input.vue:

<template>
  <div class="input-wrapper">
    <label v-if="label">{{ label }}</label>
    <input
      :type="type"
      :value="value"
      :placeholder="placeholder"
      @input="$emit('input', $event.target.value)"
    />
  </div>
</template>

<script>
export default {
  name: 'Input',
  props: {
    label: String,
    type: {
      type: String,
      default: 'text'
    },
    value: String,
    placeholder: String
  }
}
</script>

<style scoped>
.input-wrapper {
  display: flex;
  flex-direction: column;
  margin-bottom: var(--spacing-unit);
}
label {
  font-family: var(--font-family);
  font-size: var(--font-size-base);
  margin-bottom: calc(var(--spacing-unit) / 2);
}
input {
  font-family: var(--font-family);
  font-size: var(--font-size-base);
  padding: var(--spacing-unit);
  border: 1px solid var(--color-secondary);
  border-radius: calc(var(--spacing-unit) / 2);
}
</style>

And create a story for it in src/stories/Input.stories.js:

import Input from '../components/Input.vue';

export default {
  title: 'Components/Input',
  component: Input,
  argTypes: {
    label: { control: 'text' },
    type: { control: { type: 'select', options: ['text', 'password', 'email'] } },
    placeholder: { control: 'text' },
    value: { control: 'text' }
  },
};

const Template = (args, { argTypes }) => ({
  props: Object.keys(argTypes),
  components: { Input },
  template: '<Input v-bind="$props" @input="onInput" />',
  methods: {
    onInput(value) {
      console.log('Input value:', value);
    }
  }
});

export const Text = Template.bind({});
Text.args = {
  label: 'Username',
  type: 'text',
  placeholder: 'Enter your username'
};

export const Password = Template.bind({});
Password.args = {
  label: 'Password',
  type: 'password',
  placeholder: 'Enter your password'
};

export const Email = Template.bind({});
Email.args = {
  label: 'Email',
  type: 'email',
  placeholder: 'Enter your email'
};

Now we have two components in our design system. But what if we want to combine them? Let’s create a Form component that uses both Button and Input. Create a new file src/components/Form.vue:

<template>
  <form @submit.prevent="onSubmit">
    <Input v-model="username" label="Username" placeholder="Enter your username" />
    <Input v-model="password" label="Password" type="password" placeholder="Enter your password" />
    <Button type="submit">Submit</Button>
  </form>
</template>

<script>
import Button from './Button.vue';
import Input from './Input.vue';

export default {
  name: 'Form',
  components: {
    Button,
    Input
  },
  data() {
    return {
      username: '',
      password: ''
    }
  },
  methods: {
    onSubmit() {
      this.$emit('submit', { username: this.username, password: this.password })
    }
  }
}
</script>

<style scoped>
form {
  display: flex;
  flex-direction: column;
  width: 300px;
  margin: 0 auto;
}
</style>

And let’s create a story for our Form component in src/stories/Form.stories.js:

import Form from '../components/Form.vue';

export default {
  title: 'Components/Form',
  component: Form,
};

const Template = (args, { argTypes }) => ({
  props: Object.keys(argTypes),
  components: { Form },
  template: '<Form @submit="onSubmit" />',
  methods: {
    onSubmit(data) {
      console.log('Form submitted:', data);
    }
  }
});

export const Default = Template.bind({});

Now we have a small but functional design system with reusable components that all follow the same design language. This is just the beginning, though. As your design system grows, you might want to add more complex components, create variations of existing components, and document usage guidelines for your team.

One of the great things about using Storybook is that it allows you to document your components right alongside their stories. Let’s add some documentation to our Button component. Update Button.stories.js:

import Button from '../components/Button.vue';

export default {
  title: 'Components/Button',
  component: Button,
  argTypes: {
    variant: {
      control: { type: 'select', options: ['primary', 'secondary', 'danger'] }
    },
    default: { control: 'text' }
  },
  parameters: {
    docs: {
      description: {
        component: 'Button component is used for actions in forms, dialogs, and more. It supports different variants to indicate the importance of the action.'
      }
    }
  }
};

// ... rest of the file remains the same

This adds a description to the Button component in Storybook’s docs view. You can also add descriptions to individual stories or props for even more detailed documentation.

As your design system grows, you might want to add more advanced features like theme switching or responsive design. Let’s add a simple theme switcher to our design system.

First, update our variables.css:

:root {
  --color-primary: #007bff;
  --color-secondary: #6c757d;
  --color-danger: #dc3545;
  --font-family: 'Arial', sans-serif;
  --font-size-base: 16px;
  --spacing-unit: 8px;
  --background-color: white;
  --text-color: black;
}

[data-theme="dark"] {
  --color-primary: #0056b3;
  --color-secondary