Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Enforces Vue 3 Composition API best practices with script setup, TypeScript, Pinia, and Vite.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
references/component-slots.md
1---2title: Component Slots Best Practices3impact: MEDIUM4impactDescription: Poor slot API design causes empty DOM wrappers, weak TypeScript safety, brittle defaults, and unnecessary component overhead5type: best-practice6tags: [vue3, slots, components, typescript, composables]7---89# Component Slots Best Practices1011**Impact: MEDIUM** - Slots are a core component API surface in Vue. Structure them intentionally so templates stay predictable, typed, and performant.1213## Task List1415- Use shorthand syntax for named slots (`#` instead of `v-slot:`)16- Render optional slot wrapper elements only when slot content exists (`$slots` checks)17- Type scoped slot contracts with `defineSlots` in TypeScript components18- Provide fallback content for optional slots19- Prefer composables over renderless components for pure logic reuse2021## Shorthand syntax for named slots2223**BAD:**24```vue25<MyComponent>26<template v-slot:header> ... </template>27</MyComponent>28```2930**GOOD:**31```vue32<MyComponent>33<template #header> ... </template>34</MyComponent>35```3637## Conditionally Render Optional Slot Wrappers3839Use `$slots` checks when wrapper elements add spacing, borders, or layout constraints.4041**BAD:**42```vue43<!-- Card.vue -->44<template>45<article class="card">46<header class="card-header">47<slot name="header" />48</header>4950<section class="card-body">51<slot />52</section>5354<footer class="card-footer">55<slot name="footer" />56</footer>57</article>58</template>59```6061**GOOD:**62```vue63<!-- Card.vue -->64<template>65<article class="card">66<header v-if="$slots.header" class="card-header">67<slot name="header" />68</header>6970<section v-if="$slots.default" class="card-body">71<slot />72</section>7374<footer v-if="$slots.footer" class="card-footer">75<slot name="footer" />76</footer>77</article>78</template>79```8081## Type Scoped Slot Props with defineSlots8283In `<script setup lang="ts">`, use `defineSlots` so slot consumers get autocomplete and static checks.8485**BAD:**86```vue87<!-- ProductList.vue -->88<script setup lang="ts">89interface Product {90id: number91name: string92}9394defineProps<{ products: Product[] }>()95</script>9697<template>98<ul>99<li v-for="(product, index) in products" :key="product.id">100<slot :product="product" :index="index" />101</li>102</ul>103</template>104```105106**GOOD:**107```vue108<!-- ProductList.vue -->109<script setup lang="ts">110interface Product {111id: number112name: string113}114115defineProps<{ products: Product[] }>()116117defineSlots<{118default(props: { product: Product; index: number }): any119empty(): any120}>()121</script>122123<template>124<ul v-if="products.length">125<li v-for="(product, index) in products" :key="product.id">126<slot :product="product" :index="index" />127</li>128</ul>129<slot v-else name="empty" />130</template>131```132133## Provide Slot Fallback Content134135Fallback content makes components resilient when parents omit optional slots.136137**BAD:**138```vue139<!-- SubmitButton.vue -->140<template>141<button type="submit" class="btn-primary">142<slot />143</button>144</template>145```146147**GOOD:**148```vue149<!-- SubmitButton.vue -->150<template>151<button type="submit" class="btn-primary">152<slot>Submit</slot>153</button>154</template>155```156157## Prefer Composables for Pure Logic Reuse158159Renderless components are still useful for slot-driven composition, but composables are usually cleaner for logic-only reuse.160161**BAD:**162```vue163<!-- MouseTracker.vue -->164<script setup lang="ts">165import { ref, onMounted, onUnmounted } from 'vue'166167const x = ref(0)168const y = ref(0)169170function onMove(event: MouseEvent) {171x.value = event.pageX172y.value = event.pageY173}174175onMounted(() => window.addEventListener('mousemove', onMove))176onUnmounted(() => window.removeEventListener('mousemove', onMove))177</script>178179<template>180<slot :x="x" :y="y" />181</template>182```183184**GOOD:**185```ts186// composables/useMouse.ts187import { ref, onMounted, onUnmounted } from 'vue'188189export function useMouse() {190const x = ref(0)191const y = ref(0)192193function onMove(event: MouseEvent) {194x.value = event.pageX195y.value = event.pageY196}197198onMounted(() => window.addEventListener('mousemove', onMove))199onUnmounted(() => window.removeEventListener('mousemove', onMove))200201return { x, y }202}203```204205```vue206<!-- MousePosition.vue -->207<script setup lang="ts">208import { useMouse } from '@/composables/useMouse'209210const { x, y } = useMouse()211</script>212213<template>214<p>{{ x }}, {{ y }}</p>215</template>216```217