Skip to content

Component Library Template

The Component Library template provides a modern Vue 3 component library development environment with TypeScript, perfect for building reusable UI components and utility libraries.

Technical Stack

  • Vue 3 - Progressive JavaScript framework
  • TypeScript - Type-safe development
  • tsup - TypeScript bundler
  • Vite - Fast build tool and dev server
  • ESBuild - Fast JavaScript bundler
  • Tailwind CSS - Utility-first CSS framework
  • SCSS - CSS preprocessor

Quick Start

1. Create Project

bash
# Initialize project
vup init my-component-library
cd my-component-library

# Add Component Library template
vup add my-components

2. Install Dependencies

bash
# Install dependencies
pnpm install

3. Start Development

bash
# Start development server
cd apps/my-components
pnpm dev

The component library will be available at http://localhost:9301.

Project Structure

apps/my-components/
├── src/
│   ├── components/        # Vue components
│   │   ├── Input/         # Input component
│   │   │   ├── Input.vue  # Component file
│   │   │   ├── Input.ts   # Component logic
│   │   │   └── index.ts   # Component export
│   │   └── index.ts       # Components index
│   ├── libs/              # Utility libraries
│   │   ├── http/          # HTTP utilities
│   │   │   ├── index.ts   # HTTP client
│   │   │   └── types.ts   # HTTP types
│   │   └── index.ts       # Libraries index
│   ├── index.ts           # Main entry point
│   └── vue-shim.d.ts      # Vue type declarations
├── scripts/               # Build scripts
│   └── build-index.cjs    # Index generation script
├── package.json           # Dependencies and scripts
├── tsconfig.json          # TypeScript configuration
├── tsup.config.ts         # tsup configuration
└── vite.config.ts         # Vite configuration

Core Features

Component Development

Basic Component

vue
<!-- src/components/Input/Input.vue -->
<template>
  <div class="input-wrapper">
    <label v-if="label" class="input-label">{{ label }}</label>
    <input
      :type="type"
      :value="modelValue"
      :placeholder="placeholder"
      :disabled="disabled"
      :class="inputClasses"
      @input="handleInput"
      @focus="handleFocus"
      @blur="handleBlur"
    />
    <div v-if="error" class="input-error">{{ error }}</div>
  </div>
</template>

<script setup lang="ts">
import { computed } from 'vue';

interface Props {
  modelValue: string;
  label?: string;
  type?: 'text' | 'email' | 'password' | 'number';
  placeholder?: string;
  disabled?: boolean;
  error?: string;
  size?: 'small' | 'medium' | 'large';
}

const props = withDefaults(defineProps<Props>(), {
  type: 'text',
  size: 'medium',
  disabled: false,
});

const emit = defineEmits<{
  'update:modelValue': [value: string];
  focus: [event: FocusEvent];
  blur: [event: FocusEvent];
}>();

const inputClasses = computed(() => [
  'input',
  `input--${props.size}`,
  {
    'input--error': props.error,
    'input--disabled': props.disabled,
  },
]);

const handleInput = (event: Event) => {
  const target = event.target as HTMLInputElement;
  emit('update:modelValue', target.value);
};

const handleFocus = (event: FocusEvent) => {
  emit('focus', event);
};

const handleBlur = (event: FocusEvent) => {
  emit('blur', event);
};
</script>

<style lang="scss" scoped>
.input-wrapper {
  @apply w-full;
}

.input-label {
  @apply block text-sm font-medium text-gray-700 mb-1;
}

.input {
  @apply w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm;
  @apply focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500;
  @apply disabled:bg-gray-50 disabled:text-gray-500 disabled:cursor-not-allowed;

  &--small {
    @apply px-2 py-1 text-sm;
  }

  &--medium {
    @apply px-3 py-2;
  }

  &--large {
    @apply px-4 py-3 text-lg;
  }

  &--error {
    @apply border-red-300 focus:ring-red-500 focus:border-red-500;
  }

  &--disabled {
    @apply bg-gray-50 text-gray-500 cursor-not-allowed;
  }
}

.input-error {
  @apply mt-1 text-sm text-red-600;
}
</style>

Component Logic

typescript
// src/components/Input/Input.ts
import { ref, computed } from 'vue';

export interface InputProps {
  modelValue: string;
  label?: string;
  type?: 'text' | 'email' | 'password' | 'number';
  placeholder?: string;
  disabled?: boolean;
  error?: string;
  size?: 'small' | 'medium' | 'large';
}

export interface InputEmits {
  'update:modelValue': [value: string];
  focus: [event: FocusEvent];
  blur: [event: FocusEvent];
}

export function useInput(props: InputProps, emit: InputEmits) {
  const isFocused = ref(false);

  const inputClasses = computed(() => [
    'input',
    `input--${props.size}`,
    {
      'input--error': props.error,
      'input--disabled': props.disabled,
      'input--focused': isFocused.value,
    },
  ]);

  const handleInput = (event: Event) => {
    const target = event.target as HTMLInputElement;
    emit('update:modelValue', target.value);
  };

  const handleFocus = (event: FocusEvent) => {
    isFocused.value = true;
    emit('focus', event);
  };

  const handleBlur = (event: FocusEvent) => {
    isFocused.value = false;
    emit('blur', event);
  };

  return {
    isFocused,
    inputClasses,
    handleInput,
    handleFocus,
    handleBlur,
  };
}

Component Export

typescript
// src/components/Input/index.ts
export { default as Input } from './Input.vue';
export type { InputProps, InputEmits } from './Input';
export { useInput } from './Input';

Utility Library Development

HTTP Client

typescript
// src/libs/http/types.ts
export interface ApiResponse<T = any> {
  code: number;
  message: string;
  data: T;
}

export interface RequestOptions {
  method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
  headers?: Record<string, string>;
  body?: any;
  timeout?: number;
}

export interface HttpClientConfig {
  baseURL: string;
  timeout: number;
  headers: Record<string, string>;
}
typescript
// src/libs/http/index.ts
import type { ApiResponse, RequestOptions, HttpClientConfig } from './types';

export class HttpClient {
  private config: HttpClientConfig;

  constructor(config: Partial<HttpClientConfig> = {}) {
    this.config = {
      baseURL: '',
      timeout: 10000,
      headers: {
        'Content-Type': 'application/json',
      },
      ...config,
    };
  }

  async request<T = any>(
    url: string,
    options: RequestOptions = {}
  ): Promise<ApiResponse<T>> {
    const controller = new AbortController();
    const timeoutId = setTimeout(
      () => controller.abort(),
      options.timeout || this.config.timeout
    );

    try {
      const response = await fetch(`${this.config.baseURL}${url}`, {
        method: options.method || 'GET',
        headers: {
          ...this.config.headers,
          ...options.headers,
        },
        body: options.body ? JSON.stringify(options.body) : undefined,
        signal: controller.signal,
      });

      clearTimeout(timeoutId);

      if (!response.ok) {
        throw new Error(`HTTP Error: ${response.status}`);
      }

      return await response.json();
    } catch (error) {
      clearTimeout(timeoutId);
      throw error;
    }
  }

  get<T = any>(url: string, options: Omit<RequestOptions, 'method'> = {}) {
    return this.request<T>(url, { ...options, method: 'GET' });
  }

  post<T = any>(
    url: string,
    body?: any,
    options: Omit<RequestOptions, 'method' | 'body'> = {}
  ) {
    return this.request<T>(url, { ...options, method: 'POST', body });
  }

  put<T = any>(
    url: string,
    body?: any,
    options: Omit<RequestOptions, 'method' | 'body'> = {}
  ) {
    return this.request<T>(url, { ...options, method: 'PUT', body });
  }

  delete<T = any>(url: string, options: Omit<RequestOptions, 'method'> = {}) {
    return this.request<T>(url, { ...options, method: 'DELETE' });
  }
}

export const createHttpClient = (config: Partial<HttpClientConfig> = {}) => {
  return new HttpClient(config);
};

export const httpClient = createHttpClient();

Development Tools

tsup Configuration

typescript
// tsup.config.ts
import { defineConfig } from 'tsup';

export default defineConfig({
  entry: ['src/index.ts'],
  format: ['cjs', 'esm'],
  dts: true,
  splitting: false,
  sourcemap: true,
  clean: true,
  external: ['vue'],
  treeshake: true,
  minify: true,
  outDir: 'dist',
  outExtension({ format }) {
    return {
      js: `.${format}.js`,
    };
  },
});

Vite Configuration

typescript
// vite.config.ts
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import { resolve } from 'path';

export default defineConfig({
  plugins: [vue()],
  resolve: {
    alias: {
      '@': resolve(__dirname, './src'),
      '@shared': resolve(__dirname, '../../_shared'),
    },
  },
  css: {
    postcss: resolve(__dirname, '../../postcss.config.js'),
  },
  build: {
    lib: {
      entry: resolve(__dirname, 'src/index.ts'),
      name: 'MyComponents',
      fileName: (format) => `my-components.${format}.js`,
    },
    rollupOptions: {
      external: ['vue'],
      output: {
        globals: {
          vue: 'Vue',
        },
      },
    },
  },
  server: {
    host: '0.0.0.0',
    port: 9301,
    open: false,
  },
});

TypeScript Configuration

json
// tsconfig.json
{
  "extends": "../../tsconfig.json",
  "compilerOptions": {
    "outDir": "./dist",
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"],
      "@shared/*": ["../../_shared/*"]
    }
  },
  "include": [
    "src/**/*",
    "src/**/*.vue",
    "src/*.vue",
    "./auto-imports.d.ts",
    "src/vue-shim.d.ts"
  ]
}

Available Scripts

json
{
  "scripts": {
    "dev": "vite",
    "build": "tsc --noEmit && tsup",
    "build:vite": "vite build",
    "preview": "vite preview",
    "lint": "eslint src/ --ext .vue,.ts,.js",
    "lint:fix": "eslint src/ --ext .vue,.ts,.js --fix",
    "format": "prettier --write \"src/**/*.{js,ts,vue,json,css,scss}\"",
    "format:check": "prettier --check \"src/**/*.{js,ts,vue,json,css,scss}\""
  }
}

Best Practices

Component Design

vue
<template>
  <div class="component" :class="componentClasses">
    <slot name="header">
      <h3 v-if="title" class="component-title">{{ title }}</h3>
    </slot>

    <div class="component-content">
      <slot></slot>
    </div>

    <slot name="footer">
      <div v-if="$slots.footer" class="component-footer">
        <slot name="footer"></slot>
      </div>
    </slot>
  </div>
</template>

<script setup lang="ts">
import { computed } from 'vue';

interface Props {
  title?: string;
  variant?: 'default' | 'primary' | 'secondary';
  size?: 'small' | 'medium' | 'large';
  disabled?: boolean;
}

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

const componentClasses = computed(() => [
  'component',
  `component--${props.variant}`,
  `component--${props.size}`,
  {
    'component--disabled': props.disabled,
  },
]);
</script>

<style lang="scss" scoped>
.component {
  @apply border rounded-lg p-4;

  &--primary {
    @apply border-blue-500 bg-blue-50;
  }

  &--secondary {
    @apply border-gray-300 bg-gray-50;
  }

  &--small {
    @apply p-2 text-sm;
  }

  &--medium {
    @apply p-4;
  }

  &--large {
    @apply p-6 text-lg;
  }

  &--disabled {
    @apply opacity-50 cursor-not-allowed;
  }
}

.component-title {
  @apply text-lg font-semibold mb-2;
}

.component-content {
  @apply mb-4;
}

.component-footer {
  @apply pt-2 border-t border-gray-200;
}
</style>

Type Safety

typescript
// src/types/index.ts
export interface ComponentProps {
  id?: string;
  class?: string;
  style?: string | Record<string, any>;
}

export interface SizeProps {
  size?: 'small' | 'medium' | 'large';
}

export interface VariantProps {
  variant?: 'default' | 'primary' | 'secondary' | 'danger';
}

export interface DisabledProps {
  disabled?: boolean;
}

export type CommonProps = ComponentProps &
  SizeProps &
  VariantProps &
  DisabledProps;

Documentation

typescript
// src/components/Button/Button.vue
<template>
  <button
    :type="type"
    :disabled="disabled"
    :class="buttonClasses"
    @click="handleClick"
  >
    <slot name="icon" v-if="$slots.icon"></slot>
    <span v-if="$slots.default" class="button-content">
      <slot></slot>
    </span>
  </button>
</template>

<script setup lang="ts">
/**
 * Button component
 *
 * @example
 * <Button variant="primary" size="large" @click="handleClick">
 *   Click me
 * </Button>
 */
import { computed } from 'vue';

interface Props {
  type?: 'button' | 'submit' | 'reset';
  variant?: 'default' | 'primary' | 'secondary' | 'danger';
  size?: 'small' | 'medium' | 'large';
  disabled?: boolean;
}

const props = withDefaults(defineProps<Props>(), {
  type: 'button',
  variant: 'default',
  size: 'medium',
  disabled: false,
});

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

const buttonClasses = computed(() => [
  'button',
  `button--${props.variant}`,
  `button--${props.size}`,
  {
    'button--disabled': props.disabled,
  },
]);

const handleClick = (event: MouseEvent) => {
  if (!props.disabled) {
    emit('click', event);
  }
};
</script>

Build and Publishing

Build for Production

bash
# Build the component library
pnpm build

# The built files will be in the dist directory

Publish to NPM

bash
# Login to NPM
npm login

# Publish the package
npm publish

Package Configuration

json
// package.json
{
  "name": "@your-org/my-components",
  "version": "1.0.0",
  "description": "A modern Vue 3 component library",
  "main": "./dist/index.cjs.js",
  "module": "./dist/index.esm.js",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "import": "./dist/index.esm.js",
      "require": "./dist/index.cjs.js",
      "types": "./dist/index.d.ts"
    }
  },
  "files": ["dist"],
  "peerDependencies": {
    "vue": "^3.0.0"
  },
  "devDependencies": {
    "vue": "^3.0.0"
  }
}