All skills
Skillintermediate

Components

```vue <script setup lang="ts"> // Simple props interface Props { title: string count?: number items: string[] }

Claude Code Knowledge Pack7/10/2026

Overview

Components

Props with TypeScript

<script setup lang="ts">
// Simple props
interface Props {
  title: string
  count?: number
  items: string[]
}

const props = defineProps()

// Props with defaults
const propsWithDefaults = withDefaults(defineProps(), {
  count: 0,
  items: () => []
})

// Runtime props (without TypeScript)
const runtimeProps = defineProps({
  title: {
    type: String,
    required: true
  },
  count: {
    type: Number,
    default: 0,
    validator: (value: number) => value >= 0
  },
  items: {
    type: Array as PropType<string[]>,
    default: () => []
  }
})

// Access props
console.log(props.title)
console.log(props.count)
</script>

<template>
  <div>
    <h1>{{ title }}</h1>
    <p>Count: {{ count }}</p>
  </div>
</template>

Emits (Events)

<script setup lang="ts">
// TypeScript emits
interface Emits {
  (e: 'update', value: string): void
  (e: 'delete', id: number): void
  (e: 'submit', payload: { name: string; email: string }): void
}

const emit = defineEmits()

// Emit events
function handleUpdate() {
  emit('update', 'new value')
}

function handleDelete(id: number) {
  emit('delete', id)
}

function handleSubmit() {
  emit('submit', { name: 'John', email: 'john@example.com' })
}

// Runtime emits with validation
const runtimeEmit = defineEmits({
  update: (value: string) => {
    return value.length > 0
  },
  delete: (id: number) => {
    return id > 0
  }
})
</script>

<template>
  <button @click="handleUpdate">Update</button>
  <button @click="handleDelete(123)">Delete</button>
</template>

v-model (Two-way Binding)


<script setup lang="ts">

const searchQuery = ref('')
const filters = ref({ category: '', price: 0 })
</script>

<template>
  
  

  
  
</template>

<script setup lang="ts">
interface Props {
  modelValue: string
}

interface Emits {
  (e: 'update:modelValue', value: string): void
}

const props = defineProps()
const emit = defineEmits()

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

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

<script setup lang="ts">
interface Props {
  category: string
  price: number
}

interface Emits {
  (e: 'update:category', value: string): void
  (e: 'update:price', value: number): void
}

const props = defineProps()
const emit = defineEmits()
</script>

<template>
  <select
    :value="category"
    @change="emit('update:category', ($event.target as HTMLSelectElement).value)"
  >
    <option value="books">Books</option>
    <option value="electronics">Electronics</option>
  </select>
  <input
    type="number"
    :value="price"
    @input="emit('update:price', Number(($event.target as HTMLInputElement).value))"
  />
</template>

Slots


<template>
  
    <template #header>
      <h2>Card Title</h2>
    </template>

    <template #default>
      <p>Main content goes here</p>
    </template>

    <template #footer="{ close }">
      <button @click="close">Close</button>
    </template>
  
</template>

<script setup lang="ts">

const slots = useSlots()

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

function close() {
  console.log('Closing card')
}
</script>

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

    <div class="card-body">
      <slot></slot> 
    </div>

    <div v-if="hasFooter" class="card-footer">
      <slot name="footer" :close="close"></slot> 
    </div>
  </div>
</template>

Scoped Slots (Advanced)


<script setup lang="ts" generic="T">
interface Props {
  items: T[]
}

const props = defineProps()
</script>

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

<template>
  
    <template #default="{ item, index }">
      <div>{{ index }}: {{ item.name }}</div>
    </template>
  
</template>

Provide/Inject


<script setup lang="ts">

// Type-safe injection key
interface UserData {
  name: string
  email: string
}

const user = ref({
  name: 'John Doe',
  email: 'john@example.com'
})

function updateUser(newUser: UserData) {
  user.value = newUser
}

// Provide data
provide(userKey, readonly(user.value))
provide('updateUser', updateUser)
</script>

<script setup lang="ts">

// Inject with type safety
const user = inject(userKey)
const updateUser = inject<(user: UserData) => void>('updateUser')

// Inject with default value
const theme = inject('theme', 'light')

function handleUpdate() {
  if (updateUser) {
    updateUser({ name: 'Jane', email: 'jane@example.com' })
  }
}
</script>

<template>
  <div>
    <p>User: {{ user?.name }}</p>
    <p>Theme: {{ theme }}</p>
    <button @click="handleUpdate">Update User</button>
  </div>
</template>

Teleport

<script setup lang="ts">

const showModal = ref(false)
</script>

<template>
  <button @click="showModal = true">Show Modal</button>

  
  
    <div v-if="showModal" class="modal">
      <div class="modal-content">
        <h2>Modal Title</h2>
        <p>Modal content</p>
        <button @click="showModal = false">Close</button>
      </div>
    </div>
  

  
  
    <div class="notification">Notification message</div>
  

  
  
    <div>Only teleported on mobile</div>
  
</template>

<style scoped>
.modal {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: rgba(0, 0, 0, 0.5);
  display: flex;
  align-items: center;
  justify-content: center;
}

.modal-content {
  background: white;
  padding: 2rem;
  border-radius: 8px;
}
</style>

Dynamic Components

<script setup lang="ts">

// Use shallowRef for component references (performance)
const currentView = shallowRef(HomeView)

const components = {
  home: HomeView,
  about: AboutView,
  contact: ContactView
}

function switchView(view: keyof typeof components) {
  currentView.value = components[view]
}
</script>

<template>
  <button @click="switchView('home')">Home</button>
  <button @click="switchView('about')">About</button>
  <button @click="switchView('contact')">Contact</button>

  
  
    <component :is="currentView" />
  
</template>

Async Components

<script setup lang="ts">

// Lazy load component
const HeavyComponent = defineAsyncComponent(() =>
  import('./HeavyComponent.vue')
)

// With loading and error states
const AdminPanel = defineAsyncComponent({
  loader: () => import('./AdminPanel.vue'),
  loadingComponent: () => import('./LoadingSpinner.vue'),
  errorComponent: () => import('./ErrorDisplay.vue'),
  delay: 200, // Delay before showing loading component
  timeout: 3000 // Timeout before showing error
})
</script>

<template>
  
    <template #default>
      
    </template>
    <template #fallback>
      <div>Loading...</div>
    </template>
  
</template>

Quick Reference

PatternUse Case
defineProps()Type-safe props with TypeScript
withDefaults()Props with default values
defineEmits()Type-safe event emitters
v-modelTwo-way data binding
<slot>Content distribution
Scoped slotsPass data from child to parent
provide/injectDependency injection (avoid prop drilling)
``Render DOM outside component hierarchy
<component :is>Dynamic component switching
defineAsyncComponent()Lazy load components