Building a Design System with Vue Components
AI generated
<v/>
{ }
Vue.js · Design System · UI Kit · Tokens
Building a Design System with Vue Components
From design tokens to automated documentation

A collection of reusable Vue components is not yet a design system. Only design tokens as a single source of truth for colors and spacing, a deliberately designed component API and firm versioning turn loose building blocks into a system that teams can use consistently across project boundaries.

20 min read Tokens · theming · Storybook · npm package Vue 3 · Vite · Style Dictionary

1. What sets a design system apart from a component collection

Many teams believe they have a design system with Vue components as soon as they have a folder full of reusable buttons, inputs and cards. In reality that is only the visible surface. A real design system consists of three layers: design tokens as abstract design decisions, Vue components as their technical implementation, and documentation that makes both traceable for design and development. Missing any of these layers quickly reintroduces inconsistency between products.

The real value of a design system with Vue components shows once more than one project or team uses it. A single application does not need a separate package, internal components in the same repository are enough. But as soon as several Vue applications, for example a shop frontend and an internal admin tool, need to speak the same visual language, investing in a standalone, versioned, documented package pays off significantly.

A common mistake when building a design system with Vue is starting with the components instead of the tokens. Anyone who first builds ten buttons with hardcoded hex colors and then tries to retrofit a theming system has to adjust every single component. The reverse path, defining tokens first and building components on top, avoids this later rework entirely.

2. Design tokens as the single source of truth

Design tokens are named, platform independent values for colors, spacing, font sizes and radii, typically defined in JSON or YAML and transformed by a tool such as Style Dictionary into various output formats, for example CSS custom properties for Vue components and simultaneously Swift or Kotlin constants for native apps. The decisive advantage: a design decision is made in exactly one place and propagates automatically to every platform.

For a design system with Vue components, the generated CSS custom properties are referenced directly in the components, never hardcoded values. This allows theme switching at runtime without recompiling a single component, and makes design adjustments for whitelabel projects trivial: a new set of token values is enough, the Vue components themselves remain unchanged.


// tokens/color.json — Source of truth for design tokens, transformed by Style Dictionary
{
  "color": {
    "brand": {
      "primary": { "value": "#16a34a" },
      "primary-dark": { "value": "#064e3b" }
    },
    "text": {
      "default": { "value": "#1e293b" },
      "muted": { "value": "#64748b" }
    },
    "surface": {
      "default": { "value": "#ffffff" },
      "elevated": { "value": "#f8fafc" }
    }
  },
  "spacing": {
    "sm": { "value": "8px" },
    "md": { "value": "16px" },
    "lg": { "value": "24px" }
  }
}

// Generated output: dist/tokens.css (consumed by every Vue component)
// :root {
//   --color-brand-primary: #16a34a;
//   --color-text-default: #1e293b;
//   --spacing-md: 16px;
// }

3. Designing the component API deliberately

The props and slots signature of a Vue component in a design system is a long term contract, not an internal implementation detail. Once a button from the design system is used across dozens of Vue applications, every change to its props signature turns into breaking change communication across team boundaries. That is why the API of every component should be designed for stability from the start: named, semantic prop values such as variant="primary" instead of arbitrary external CSS classes, and explicit slots for extensibility instead of adding a new prop for every special case.

A proven pattern for Vue design system components is to control styling properties through strictly typed props, while arbitrary content is inserted via slots. A button component defines, for example, variant, size and disabled as props with a limited value range, while the actual button text comes through the default slot. This prevents consumers from injecting arbitrary Tailwind classes from the outside and thereby undermining the visual consistency the design system is meant to guarantee.


// DesignSystemButton.vue — API designed for stability, not for flexibility at any cost
<script setup lang="ts">
type Variant = 'primary' | 'secondary' | 'danger' | 'ghost';
type Size = 'sm' | 'md' | 'lg';

interface Props {
  variant?: Variant;
  size?: Size;
  disabled?: boolean;
  loading?: boolean;
}

const props = withDefaults(defineProps<Props>(), {
  variant: 'primary',
  size: 'md',
  disabled: false,
  loading: false,
});

const emit = defineEmits<{ click: [event: MouseEvent] }>();

// Only design-system-owned class names — consumers cannot inject arbitrary utility classes
const variantClass = computed(() => `ds-btn--${props.variant}`);
const sizeClass = computed(() => `ds-btn--${props.size}`);
</script>
<template>
  <button
    :class="['ds-btn', variantClass, sizeClass, { 'ds-btn--loading': loading }]"
    :disabled="disabled || loading"
    @click="emit('click', $event)"
  >
    <slot name="icon-left" />
    <slot />
    <slot name="icon-right" />
  </button>
</template>

4. Theming and dark mode via CSS variables

Theming in a Vue design system should never be solved through conditional props such as dark-mode="true" in every single component. The more robust path is a wrapper attribute, for example data-theme="dark" on the application's root element, combined with CSS custom properties that take different values per theme. Every design system component references the same variable names, regardless of the active theme, and does not need to know anything about the theming mechanism itself.

For whitelabel scenarios with multiple brands, the same mechanism works: an additional attribute such as data-brand="acme" loads a different set of token values without a single Vue component needing to be adjusted. This decoupling between visual appearance and component logic is one of the most important architectural decisions when building a long lived design system with Vue.


/* tokens/theme.css — Same variable names, different values per theme attribute */
:root,
[data-theme="light"] {
  --color-surface-default: #ffffff;
  --color-text-default: #1e293b;
  --color-brand-primary: #16a34a;
}

[data-theme="dark"] {
  --color-surface-default: #0f172a;
  --color-text-default: #e2e8f0;
  --color-brand-primary: #4ade80;
}

/* Whitelabel: a second attribute swaps brand values independently of the theme */
[data-brand="acme"] {
  --color-brand-primary: #db2777;
}

/* Every Vue component references only the variable name, never a raw hex value */
.ds-btn--primary {
  background: var(--color-brand-primary);
  color: var(--color-surface-default);
}

5. Package structure and build setup

A design system with Vue components is usually shipped as a standalone npm package with Vite as a library build. It is important to declare Vue itself as a peerDependency, not as a regular dependency, so consuming applications bring their own Vue version instead of loading a second Vue instance from the design system package. The build typically produces both an ESM and a CommonJS bundle, to support both modern Vite projects and older Webpack setups.

The internal structure of the package separates tokens, components and composables into their own subfolders with their own export entry points. This enables tree shaking: an application that only uses the button and the input field does not need to load the entire design system bundle, but imports individual components deliberately via subpath exports.


// vite.config.js — Library build for the design system package
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import dts from 'vite-plugin-dts';

export default defineConfig({
  plugins: [vue(), dts({ include: ['src'] })],
  build: {
    lib: {
      entry: {
        index: 'src/index.ts',
        button: 'src/components/Button/index.ts',
        input: 'src/components/Input/index.ts',
      },
      formats: ['es', 'cjs'],
    },
    rollupOptions: {
      // Vue must never be bundled into the design system itself
      external: ['vue'],
      output: { globals: { vue: 'Vue' } },
    },
  },
});

// package.json — vue declared as peer dependency, never a direct one
// "peerDependencies": { "vue": "^3.4.0" }

6. Versioning and breaking change communication

Semantic versioning is not optional for a design system with Vue components, because dozens of consuming applications need to rely on stable major versions. Every change to props, slots or the visual appearance of a component must be classified: a new optional prop is a minor release, a changed prop semantic or a removed slot is a major release. Changesets or a comparable tool automates this classification and generates a consistent changelog from it.

For major releases of a Vue design system, a transition period with codemods that automatically migrate consuming applications to the new API is recommended, instead of manually walking every team through the breaking changes. Without this investment, teams frequently get stuck on outdated major versions because the manual migration seems too costly, which in the long run reintroduces the fragmentation the design system was meant to prevent.


#!/usr/bin/env bash
# Publishing a new design system version with automated changelog generation
set -euo pipefail

# Changesets reads the pending change files and determines the version bump
npx changeset version

# Classify: patch = bugfix, minor = new optional prop, major = removed slot/prop
npm run build
npm run test
npm run test:visual -- --ci

npx changeset publish
echo "[OK] Published new design system version with generated changelog"

7. Automating documentation with Storybook

Without living documentation, a design system with Vue components quickly loses adoption, because developers would rather build their own component than look up the props of an undocumented existing one in the source code. Storybook is the de facto standard for Vue design systems, because it renders every component in isolation, makes all props interactively changeable through controls and can derive variant combinations automatically from TypeScript types.

It is important to use Storybook not just as a showcase but as a testing instrument: visual regression tests via Chromatic or Playwright catch unintended visual changes to design system components before they become visible in production across dozens of applications. Documentation should also include usage guidelines, not just technical props, such as when variant="danger" is appropriate and when it is not.

8. Governance: contributions and acceptance criteria

A design system with Vue components without a clear contribution process either becomes a bottleneck, because a single team has to review every change, or turns into sprawl, because every team pushes its own components into the shared package. A working governance model defines explicit acceptance criteria: a new component needs design sign off, an accessibility review, Storybook documentation and at least two independent consuming applications before it is accepted into the core package.

Many successful design system teams additionally establish office hours or an RFC process for larger API changes, so consuming teams can influence decisions early instead of being surprised by a breaking change only during migration. This organizational discipline is often, in the end, more important for long term success than any single technical decision about the Vue components themselves.

9. Design system approaches compared

There are different levels of maturity for running a design system with Vue components, from a simple component collection in a monorepo to a fully standalone, independently versioned product with its own team.

Approach Consistency Maintenance effort Suitability
Copy paste components Low Low per copy Single project, prototype
Shared folder in a monorepo Medium Medium A few related projects
Standalone npm package High Increased, dedicated team useful Multiple independent products
Token driven system Very high High, tooling required Multi platform, whitelabel

Most Vue organizations grow organically from the copy paste phase through a shared folder to a standalone npm package. A token driven system with Style Dictionary only pays off once multi platform consistency or whitelabeling is explicitly required, because the additional tooling brings a noticeable maintenance burden.

Mironsoft

Design systems, Vue component libraries and frontend consistency

A design system teams actually use?

We build design tokens, Vue component API and Storybook documentation as a versioned package that consistently serves multiple applications and stays maintainable long term.

Tokens & theming

Building design tokens with Style Dictionary and dark mode capable theming

Component API

Stable, typed Vue components with a long term viable contract

Docs & governance

Storybook setup and contribution process for sustainable adoption

10. Summary

A design system with Vue components is more than a collection of reusable building blocks. Design tokens as the single source of truth for colors, spacing and radii prevent hardcoded values in components and enable theming without recompilation. The component API should be designed for stability from the start, with a limited value range for props and slots for flexible content instead of arbitrary external CSS injection.

Vue as a peerDependency, a build with ESM and CommonJS output, and subpath exports for tree shaking form the technical foundation. Semantic versioning with clear breaking change communication, automated Storybook documentation with visual regression tests, and a defined governance process are ultimately just as decisive as the code quality of the individual Vue components.

Building a Design System with Vue Components — the essentials at a glance

Design tokens

Single source of truth for colors and spacing, generated via Style Dictionary into CSS variables.

Component API

Limited, semantic props instead of arbitrary external CSS classes, slots for content.

Versioning

Semantic versioning with changesets, codemods for major releases ease migrations.

Documentation

Storybook with controls, visual regression tests and usage guidelines.

11. FAQ: Building a Design System with Vue Components

1Design system vs. component library?
A design system combines tokens, components and documentation. A pure library only delivers technical building blocks.
2When does a standalone package pay off?
As soon as more than one Vue application uses the same visual language.
3Why design tokens instead of hex values?
A single source of truth propagates changes automatically to all platforms and components.
4Why Vue as a peerDependency?
Prevents a second Vue instance from the package and thus reactivity bugs and duplicate bundling.
5How to prevent API misuse?
Limited, semantic props instead of external CSS classes, slots for flexible content.
6How does theming work without props?
Via a data-theme attribute on the root and CSS custom properties, independent of the components themselves.
7How to communicate breaking changes?
Semantic versioning with changesets and, where possible, codemods for automated migration.
8Why is Storybook standard?
Isolated rendering, interactive controls and visual regression tests via Chromatic or Playwright.
9Who may contribute?
Any team, provided design sign off, accessibility review, documentation and multiple consumers exist.
10Token system for a single project?
Rarely worthwhile, pays off only with multi platform or whitelabel requirements.