Claude
Skills
Sign in
Back

vue-component-patterns

Included with Lifetime
$97 forever

Use when Vue component patterns including props, emits, slots, and provide/inject. Use when building reusable Vue components.

Design

What this skill does


# Vue Component Patterns

Master Vue component patterns to build reusable, maintainable components
with proper prop validation, events, and composition.

## Props Patterns

### Basic Props with TypeScript

```typescript
<script setup lang="ts">
interface Props {
  title: string;
  count?: number;
  items: string[];
}

const props = withDefaults(defineProps<Props>(), {
  count: 0,
  items: () => []
});
</script>

<template>
  <div>
    <h2>{{ title }}</h2>
    <p>Count: {{ count }}</p>
    <ul>
      <li v-for="item in items" :key="item">{{ item }}</li>
    </ul>
  </div>
</template>
```

### Advanced Prop Types

```typescript
<script setup lang="ts">
import type { PropType } from 'vue';

type Status = 'pending' | 'success' | 'error';

interface User {
  id: number;
  name: string;
  email: string;
}

interface Props {
  // Literal types
  status: Status;

  // Complex objects
  user: User;

  // Functions
  onUpdate: (value: string) => void;

  // Generic arrays
  tags: string[];

  // Object arrays
  users: User[];

  // Nullable
  description: string | null;

  // Union types
  value: string | number;
}

const props = defineProps<Props>();
</script>
```

### Runtime Props Validation

```typescript
<script setup lang="ts">
import type { PropType } from 'vue';

type ButtonSize = 'sm' | 'md' | 'lg';

const props = defineProps({
  // Type checking
  title: {
    type: String,
    required: true
  },

  // Default values
  count: {
    type: Number,
    default: 0
  },

  // Multiple types
  value: {
    type: [String, Number],
    required: true
  },

  // Object with type
  user: {
    type: Object as PropType<{ name: string; age: number }>,
    required: true
  },

  // Array with type
  tags: {
    type: Array as PropType<string[]>,
    default: () => []
  },

  // Custom validator
  size: {
    type: String as PropType<ButtonSize>,
    default: 'md',
    validator: (value: string) => ['sm', 'md', 'lg'].includes(value)
  },

  // Complex validator
  email: {
    type: String,
    validator: (value: string) => {
      return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
    }
  },

  // Function prop
  onClick: {
    type: Function as PropType<(id: number) => void>,
    required: false
  }
});
</script>
```

### Props with Defaults

```typescript
<script setup lang="ts">
interface Props {
  title?: string;
  count?: number;
  items?: string[];
  user?: {
    name: string;
    email: string;
  };
  options?: {
    enabled: boolean;
    timeout: number;
  };
}

// Simple defaults
const props = withDefaults(defineProps<Props>(), {
  title: 'Default Title',
  count: 0
});

// Function defaults for objects/arrays
const propsWithComplex = withDefaults(defineProps<Props>(), {
  title: 'Default',
  count: 0,
  items: () => [],
  user: () => ({ name: 'Guest', email: '[email protected]' }),
  options: () => ({ enabled: true, timeout: 5000 })
});
</script>
```

## Emits Patterns

### TypeScript Emits

```typescript
<script setup lang="ts">
// Define emit types
const emit = defineEmits<{
  // No payload
  close: [];

  // Single payload
  update: [value: string];

  // Multiple payloads
  change: [id: number, value: string];

  // Object payload
  submit: [data: { name: string; email: string }];
}>();

function handleClose() {
  emit('close');
}

function handleUpdate(value: string) {
  emit('update', value);
}

function handleChange(id: number, value: string) {
  emit('change', id, value);
}

function handleSubmit() {
  emit('submit', { name: 'John', email: '[email protected]' });
}
</script>
```

### Runtime Emits Validation

```typescript
<script setup lang="ts">
const emit = defineEmits({
  // Basic event
  click: null,

  // Validation
  update: (value: number) => {
    return value >= 0;
  },

  // Complex validation
  submit: (payload: { email: string; password: string }) => {
    if (!payload.email || !payload.password) {
      console.warn('Invalid submit payload');
      return false;
    }
    return true;
  }
});
</script>
```

### Custom v-model

```typescript
<!-- CustomInput.vue -->
<script setup lang="ts">
interface Props {
  modelValue: string;
}

const props = defineProps<Props>();

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

function handleInput(e: Event) {
  const target = e.target as HTMLInputElement;
  emit('update:modelValue', target.value);
}
</script>

<template>
  <input
    :value="modelValue"
    @input="handleInput"
    type="text"
  />
</template>

<!-- Usage -->
<script setup lang="ts">
import { ref } from 'vue';
import CustomInput from './CustomInput.vue';

const text = ref('');
</script>

<template>
  <CustomInput v-model="text" />
</template>
```

### Multiple v-models

```typescript
<!-- RangeSlider.vue -->
<script setup lang="ts">
interface Props {
  min: number;
  max: number;
}

const props = defineProps<Props>();

const emit = defineEmits<{
  'update:min': [value: number];
  'update:max': [value: number];
}>();
</script>

<template>
  <div>
    <input
      type="range"
      :value="min"
      @input="emit('update:min', Number($event.target.value))"
    />
    <input
      type="range"
      :value="max"
      @input="emit('update:max', Number($event.target.value))"
    />
  </div>
</template>

<!-- Usage -->
<script setup lang="ts">
import { ref } from 'vue';

const minValue = ref(0);
const maxValue = ref(100);
</script>

<template>
  <RangeSlider v-model:min="minValue" v-model:max="maxValue" />
</template>
```

## Slots Patterns

### Basic Slots

```typescript
<!-- Card.vue -->
<template>
  <div class="card">
    <header v-if="$slots.header">
      <slot name="header" />
    </header>
    <main>
      <slot />
    </main>
    <footer v-if="$slots.footer">
      <slot name="footer" />
    </footer>
  </div>
</template>

<!-- Usage -->
<template>
  <Card>
    <template #header>
      <h1>Card Title</h1>
    </template>

    <p>Card content goes here</p>

    <template #footer>
      <button>Action</button>
    </template>
  </Card>
</template>
```

### Scoped Slots

```typescript
<!-- List.vue -->
<script setup lang="ts" generic="T">
interface Props {
  items: T[];
}

const props = defineProps<Props>();
</script>

<template>
  <div>
    <div v-for="(item, index) in items" :key="index">
      <slot :item="item" :index="index" />
    </div>
  </div>
</template>

<!-- Usage -->
<script setup lang="ts">
interface User {
  id: number;
  name: string;
  email: string;
}

const users: User[] = [
  { id: 1, name: 'John', email: '[email protected]' },
  { id: 2, name: 'Jane', email: '[email protected]' }
];
</script>

<template>
  <List :items="users">
    <template #default="{ item, index }">
      <div>
        {{ index + 1 }}. {{ item.name }} - {{ item.email }}
      </div>
    </template>
  </List>
</template>
```

### Fallback Slot Content

```typescript
<!-- Button.vue -->
<template>
  <button>
    <slot>
      Click Me
    </slot>
  </button>
</template>

<!-- Custom content -->
<Button>Custom Text</Button>

<!-- Uses fallback -->
<Button />
```

### Dynamic Slots

```typescript
<!-- DynamicSlots.vue -->
<script setup lang="ts">
import { useSlots } from 'vue';

const slots = useSlots();

// Check if slot exists
const hasHeader = !!slots.header;

// Access slot props
const headerProps = slots.header?.();
</script>

<template>
  <div>
    <div v-if="hasHeader" class="header">
      <slot name="header" />
    </div>
    <slot />
  </div>
</template>
```

### Renderless Components with Slots

```typescript
<!-- Mouse.vue - Renderless component -->
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue';

const x = ref(0);
const y = ref(0);

function update(event: MouseEvent) {
  x.value = event.pageX;
  y.value = event.pageY;
}

onMounted(() => {
  window.addEventListener('mousemove', update);
});

onUnmounted(() => {
  window.removeEventListener('mousemove', update);
});
</script>

<template>
  <slot :x="x" :y="y" />
</template>

<!-- Usage -->
<template>
  <Mouse v-slot="{ x, y }">

Related in Design