Skip to content
← All questions
🔥 Advanced

How would you design a shared component library?

🧩 Components🏗️ Architecture⚡ Vite🧪 Vitest🎰 Slots

A shared component library gives multiple apps (or teams) a consistent set of UI components. The goal is reusability without coupling. Each consumer imports what they need, gets type safety and theming, and can upgrade on their own schedule.

Project structure

The library lives in its own package, either a standalone repo or a workspace within a monorepo. Each component is a self-contained unit with typed props, typed emits, and slots for flexibility:

text
packages/ui/
├── src/
│   ├── components/
│   │   ├── Button/
│   │   │   ├── Button.vue
│   │   │   └── index.ts
│   │   ├── Input/
│   │   └── Modal/
│   ├── tokens/
│   │   └── colors.css
│   └── index.ts          ← named exports for tree-shaking
├── package.json
└── vite.config.ts         ← library mode build

Component design principles

Type everything. Every component should have typed props, typed emits, and typed slots. Consumers get autocomplete and compile-time errors:

vue
<script setup lang="ts">
interface Props {
  variant?: 'primary' | 'secondary' | 'danger'
  size?: 'sm' | 'md' | 'lg'
  disabled?: boolean
}

withDefaults(defineProps<Props>(), {
  variant: 'primary',
  size: 'md'
})

defineEmits<{
  click: [event: MouseEvent]
}>()

defineSlots<{
  default: () => void
  icon?: () => void
}>()
</script>
Open in Vue Playground

Use slots over props for content. A button that takes label and icon props is limited. A button with a default slot and an icon slot lets consumers put anything inside: text, icons, badges, complex markup.

Use CSS custom properties for theming. Don't hardcode colors or spacing. Expose design tokens that consumers can override:

css
.btn {
  background: var(--ui-color-primary, #3b82f6);
  padding: var(--ui-spacing-md, 0.75rem 1.5rem);
  border-radius: var(--ui-radius, 0.375rem);
}

Consumers override tokens at the app level without touching library code.

Build and distribution

Use Vite library mode to build. Export named components for tree-shaking so consumers only bundle what they import:

ts
// src/index.ts
export { default as Button } from './components/Button/Button.vue'
export { default as Input } from './components/Input/Input.vue'
export { default as Modal } from './components/Modal/Modal.vue'

Set sideEffects: false in package.json so bundlers can eliminate unused components.

Documentation

Use VitePress or Storybook for interactive docs. Each component should have live examples showing variants, sizes, slots, and edge cases. Storybook is more feature-rich for component development (visual testing, accessibility checks); VitePress is lighter if your docs also cover usage guides and API reference.

Versioning and testing

  • Semver: breaking prop changes are major bumps, new components are minor
  • Vitest for unit tests (prop rendering, emit behavior, slot content)
  • Visual regression (Chromatic, Percy, or Playwright screenshots) to catch unintended style changes

See also: How do you declare props with TypeScript? · What are slots? · What are fallthrough attributes?

References

Released under the MIT License.