Vue 3 Slots : Guide Complet de la Composition de Composants

Les slots sont l'une des fonctionnalités les plus puissantes de Vue pour construire des composants réutilisables et flexibles.

27 Oct, 2025 5min min de lecture
Vue 3 Slots : Guide Complet de la Composition de Composants

Vue 3 Slots : Guide Complet de la Composition de Composants

Table des matières

Introduction

Les slots sont l'une des fonctionnalités les plus puissantes de Vue pour construire des composants réutilisables et flexibles. Ils permettent aux composants parents de passer du contenu dans les composants enfants, créant une séparation claire entre la logique et la présentation des composants. Ce guide explore les slots depuis les bases jusqu'aux patterns avancés.

Qu'est-ce qu'un Slot ?

Les slots sont l'implémentation par Vue de l'API de distribution de contenu des Web Components. Ils agissent comme des emplacements réservés dans un composant enfant où les composants parents peuvent injecter du contenu personnalisé.

Syntaxe de Base

<!-- Button.vue -->
<template>
  <button class="btn">
    <slot></slot>
  </button>
</template>

<!-- Composant parent -->
<template>
  <Button>Cliquez-moi</Button>
</template>

Le texte "Cliquez-moi" est distribué dans l'élément <slot>.

Contenu par Défaut

Les slots peuvent avoir un contenu de secours qui s'affiche quand le parent ne fournit rien :

<template>
  <button>
    <slot>Texte par défaut</slot>
  </button>
</template>

Slots Nommés

Quand vous avez besoin de plusieurs points de distribution de contenu, utilisez des slots nommés :

<!-- Card.vue -->
<template>
  <div class="card">
    <header class="card-header">
      <slot name="header"></slot>
    </header>
    
    <main class="card-body">
      <slot></slot> <!-- slot par défaut -->
    </main>
    
    <footer class="card-footer">
      <slot name="footer"></slot>
    </footer>
  </div>
</template>

<!-- Utilisation -->
<template>
  <Card>
    <template #header>
      <h3>Titre de la carte</h3>
    </template>
    
    <p>Ceci va dans le slot par défaut</p>
    
    <template #footer>
      <button>Action</button>
    </template>
  </Card>
</template>

Note : #header est un raccourci pour v-slot:header.

Scoped Slots

Les scoped slots (slots avec portée) permettent aux composants enfants de renvoyer des données au contenu du slot parent. C'est une fonctionnalité puissante pour créer des composants de liste personnalisables, des tableaux de données, et bien plus encore.

<!-- TodoList.vue -->
<template>
  <ul>
    <li v-for="(todo, index) in todos" :key="todo.id">
      <slot 
        :todo="todo" 
        :index="index"
        :toggle="() => toggleTodo(todo.id)"
      ></slot>
    </li>
  </ul>
</template>

<script setup>
const todos = ref([
  { id: 1, text: 'Apprendre Vue', completed: false },
  { id: 2, text: 'Maîtriser les Slots', completed: false }
]);

const toggleTodo = (id) => {
  const todo = todos.value.find(t => t.id === id);
  if (todo) todo.completed = !todo.completed;
};
</script>

<!-- Utilisation -->
<template>
  <TodoList>
    <template #default="{ todo, index, toggle }">
      <input type="checkbox" @change="toggle" />
      <span :class="{ completed: todo.completed }">
        {{ index + 1 }}. {{ todo.text }}
      </span>
    </template>
  </TodoList>
</template>

Déstructuration des Props de Slot

Vous pouvez déstructurer les props de slot pour une syntaxe plus propre :

<!-- Au lieu de ceci -->
<template #default="slotProps">
  {{ slotProps.todo.text }}
</template>

<!-- Utilisez ceci -->
<template #default="{ todo, index }">
  {{ todo.text }}
</template>

Patterns Avancés de Slots

Composants Sans Rendu (Renderless)

Les composants sans rendu fournissent la logique sans imposer de structure de markup :

<!-- MouseTracker.vue -->
<template>
  <slot :x="x" :y="y"></slot>
</template>

<script setup>
import { ref, onMounted, onUnmounted } from 'vue';

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

const updateMouse = (e) => {
  x.value = e.pageX;
  y.value = e.pageY;
};

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

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

<!-- Utilisation -->
<template>
  <MouseTracker v-slot="{ x, y }">
    <p>Position de la souris : {{ x }}, {{ y }}</p>
  </MouseTracker>
</template>

Noms de Slots Dynamiques

Vous pouvez lier les noms de slots dynamiquement :

<template>
  <Card>
    <template #[dynamicSlotName]>
      <p>Contenu dynamique</p>
    </template>
  </Card>
</template>

<script setup>
const dynamicSlotName = ref('header');
</script>

Slots Conditionnels avec $slots

Vérifiez si un slot a du contenu avant de rendre les éléments enveloppants :

<template>
  <div class="card">
    <header v-if="$slots.header" class="card-header">
      <slot name="header"></slot>
    </header>
    
    <main class="card-body">
      <slot></slot>
    </main>
    
    <footer v-if="$slots.footer" class="card-footer">
      <slot name="footer"></slot>
    </footer>
  </div>
</template>

<script setup>
import { useSlots } from 'vue';

const slots = useSlots();

// Vous pouvez aussi vérifier par programmation
if (slots.header) {
  console.log('Le slot header est fourni');
}
</script>

Props de Slot avec Valeurs Calculées

Passez des données calculées via des scoped slots :

<template>
  <div>
    <slot 
      :filteredItems="filteredItems"
      :totalCount="items.length"
      :filterCount="filteredItems.length"
    ></slot>
  </div>
</template>

<script setup>
import { computed } from 'vue';

const props = defineProps({
  items: Array,
  filter: String
});

const filteredItems = computed(() => {
  if (!props.filter) return props.items;
  return props.items.filter(item => 
    item.name.toLowerCase().includes(props.filter.toLowerCase())
  );
});
</script>

Transmission de Slots

Lors de la création de composants enveloppeurs, vous devez souvent transmettre les slots aux composants enfants :

<!-- FancyCard.vue -->
<template>
  <div class="fancy-wrapper">
    <Card>
      <!-- Transmet tous les slots -->
      <template v-for="(_, name) in $slots" #[name]="slotProps">
        <slot :name="name" v-bind="slotProps"></slot>
      </template>
    </Card>
  </div>
</template>

Considérations de Performance

Évitez les Calculs Lourds dans le Contenu des Slots

Le contenu des slots est réévalué à chaque rendu. Gardez les calculs légers ou utilisez des propriétés calculées :

<!-- ❌ À éviter -->
<template>
  <DataTable>
    <template #default="{ item }">
      {{ expensiveOperation(item) }}
    </template>
  </DataTable>
</template>

<!-- ✅ Mieux -->
<template>
  <DataTable>
    <template #default="{ item }">
      {{ computedValues[item.id] }}
    </template>
  </DataTable>
</template>

<script setup>
const computedValues = computed(() => {
  return items.value.reduce((acc, item) => {
    acc[item.id] = expensiveOperation(item);
    return acc;
  }, {});
});
</script>

Mémoïsation des Slots

Pour un contenu de slot coûteux, considérez la mémoïsation :

<script setup>
const memoizedSlotContent = computed(() => {
  return heavyComputation(props.data);
});
</script>

<template>
  <Component>
    <template #default>
      {{ memoizedSlotContent }}
    </template>
  </Component>
</template>

Exemples Concrets

Tableau de Données avec Colonnes Personnalisables

<!-- DataTable.vue -->
<template>
  <table>
    <thead>
      <tr>
        <th v-for="column in columns" :key="column.key">
          <slot :name="`header-${column.key}`" :column="column">
            {{ column.label }}
          </slot>
        </th>
      </tr>
    </thead>
    <tbody>
      <tr v-for="(row, rowIndex) in data" :key="rowIndex">
        <td v-for="column in columns" :key="column.key">
          <slot 
            :name="`cell-${column.key}`" 
            :value="row[column.key]"
            :row="row"
            :column="column"
          >
            {{ row[column.key] }}
          </slot>
        </td>
      </tr>
    </tbody>
  </table>
</template>

<script setup>
defineProps({
  columns: Array,
  data: Array
});
</script>

<!-- Utilisation -->
<template>
  <DataTable :columns="columns" :data="users">
    <template #cell-email="{ value }">
      <a :href="`mailto:${value}`">{{ value }}</a>
    </template>
    
    <template #cell-status="{ row }">
      <span :class="row.status">{{ row.status }}</span>
    </template>
    
    <template #cell-actions="{ row }">
      <button @click="editUser(row)">Éditer</button>
      <button @click="deleteUser(row)">Supprimer</button>
    </template>
  </DataTable>
</template>

Modal avec Plusieurs Variantes

<!-- Modal.vue -->
<template>
  <Teleport to="body">
    <div v-if="isOpen" class="modal-overlay" @click="handleOverlayClick">
      <div class="modal-container" @click.stop>
        <div v-if="$slots.header" class="modal-header">
          <slot name="header"></slot>
          <button 
            v-if="closable" 
            class="modal-close"
            @click="close"
          >
            ×
          </button>
        </div>
        
        <div class="modal-body">
          <slot></slot>
        </div>
        
        <div v-if="$slots.footer" class="modal-footer">
          <slot name="footer" :close="close"></slot>
        </div>
      </div>
    </div>
  </Teleport>
</template>

<script setup>
const props = defineProps({
  isOpen: Boolean,
  closable: {
    type: Boolean,
    default: true
  },
  closeOnOverlay: {
    type: Boolean,
    default: true
  }
});

const emit = defineEmits(['close']);

const close = () => emit('close');

const handleOverlayClick = () => {
  if (props.closeOnOverlay) close();
};
</script>

Composant Accordéon

<!-- Accordion.vue -->
<template>
  <div class="accordion">
    <slot 
      :toggle="toggle"
      :isOpen="isOpen"
      :activeIndex="activeIndex"
    ></slot>
  </div>
</template>

<script setup>
import { ref } from 'vue';

const props = defineProps({
  multiple: {
    type: Boolean,
    default: false
  }
});

const activeIndex = ref(props.multiple ? [] : null);

const isOpen = (index) => {
  if (props.multiple) {
    return activeIndex.value.includes(index);
  }
  return activeIndex.value === index;
};

const toggle = (index) => {
  if (props.multiple) {
    const idx = activeIndex.value.indexOf(index);
    if (idx > -1) {
      activeIndex.value.splice(idx, 1);
    } else {
      activeIndex.value.push(index);
    }
  } else {
    activeIndex.value = activeIndex.value === index ? null : index;
  }
};
</script>

<!-- Utilisation -->
<template>
  <Accordion v-slot="{ toggle, isOpen }">
    <div class="accordion-item">
      <button @click="toggle(0)">Section 1</button>
      <div v-if="isOpen(0)" class="content">Contenu 1</div>
    </div>
    <div class="accordion-item">
      <button @click="toggle(1)">Section 2</button>
      <div v-if="isOpen(1)" class="content">Contenu 2</div>
    </div>
  </Accordion>
</template>

Bonnes Pratiques

  1. Nommez les slots de manière descriptive : Utilisez des noms clairs comme header, footer, actions plutôt que des noms génériques.

  2. Documentez les props des slots : Documentez toujours quelles props vos scoped slots fournissent.

  3. Fournissez du contenu de secours : Utilisez du contenu de slot par défaut pour une meilleure expérience développeur.

  4. Gardez les APIs de slots stables : Changer les noms ou la structure des props de slot est un changement cassant (breaking change).

  5. Utilisez les scoped slots pour les données : Passez des données via les scoped slots, pas seulement pour les variations de style.

  6. Considérez la composition de slots : Divisez les composants complexes en composants plus petits avec des APIs de slots ciblées.

  7. Testez le contenu des slots : Assurez-vous que vos composants fonctionnent avec divers scénarios de contenu de slot.

  8. Évitez la pollution des props de slot : Ne passez que les données nécessaires via les props de slot.

  9. Utilisez TypeScript pour la sécurité des types : Définissez les types des props de slot pour un meilleur support IDE.

  10. Documentez le comportement des slots : Expliquez clairement comment les slots interagissent avec l'état du composant.

Pièges Courants

  1. Oublier v-bind pour les scoped slots : Utilisez toujours v-bind ou : lors du passage de plusieurs props.
<!-- ❌ Incorrect -->
<slot user="user" index="index"></slot>

<!-- ✅ Correct -->
<slot :user="user" :index="index"></slot>
  1. Surutilisation des slots : Tout n'a pas besoin d'être un slot. Utilisez des props pour les données simples.

  2. Conflits de noms de slots : Soyez prudent avec les noms de slots dynamiques pour éviter les conflits.

  3. Ne pas vérifier l'existence des slots : Utilisez $slots ou useSlots() pour vérifier avant de rendre les éléments enveloppants.

  4. Muter les props de slot : Ne modifiez pas les props passées via les scoped slots dans le parent.

<!-- ❌ À éviter -->
<template #default="{ user }">
  <button @click="user.active = !user.active">Basculer</button>
</template>

<!-- ✅ Mieux -->
<template #default="{ user, toggleActive }">
  <button @click="toggleActive(user.id)">Basculer</button>
</template>
  1. Logique complexe dans le contenu des slots : Gardez le contenu des slots simple et déplacez la logique complexe vers des propriétés calculées ou des méthodes.

  2. Oublier la réactivité : Rappelez-vous que le contenu des slots est réactif aux changements des props de slot.

Techniques Avancées

Pattern de Composition de Slots

Construisez des interfaces utilisateur complexes en composant plusieurs composants basés sur des slots :

<!-- Layout.vue -->
<template>
  <div class="layout">
    <Sidebar>
      <template #header>
        <slot name="sidebar-header"></slot>
      </template>
      <slot name="sidebar"></slot>
    </Sidebar>
    
    <MainContent>
      <template #header>
        <slot name="page-header"></slot>
      </template>
      <slot></slot>
    </MainContent>
  </div>
</template>

Slots de Fonctions

Passez des fonctions via des scoped slots pour plus de flexibilité :

<!-- FormField.vue -->
<template>
  <div class="field">
    <slot 
      :value="modelValue"
      :updateValue="updateValue"
      :errors="errors"
      :validate="validate"
    ></slot>
  </div>
</template>

<script setup>
const updateValue = (newValue) => {
  emit('update:modelValue', newValue);
  validate(newValue);
};

const validate = (value) => {
  // Logique de validation
};
</script>

Gestion d'État Basée sur les Slots

Créez des composants qui gèrent l'état et l'exposent via des slots :

<!-- Pagination.vue -->
<template>
  <slot
    :currentPage="currentPage"
    :totalPages="totalPages"
    :goToPage="goToPage"
    :nextPage="nextPage"
    :prevPage="prevPage"
    :isFirstPage="isFirstPage"
    :isLastPage="isLastPage"
  ></slot>
</template>

<script setup>
import { computed, ref } from 'vue';

const props = defineProps({
  totalItems: Number,
  itemsPerPage: { type: Number, default: 10 }
});

const currentPage = ref(1);

const totalPages = computed(() => 
  Math.ceil(props.totalItems / props.itemsPerPage)
);

const isFirstPage = computed(() => currentPage.value === 1);
const isLastPage = computed(() => currentPage.value === totalPages.value);

const goToPage = (page) => {
  if (page >= 1 && page <= totalPages.value) {
    currentPage.value = page;
  }
};

const nextPage = () => goToPage(currentPage.value + 1);
const prevPage = () => goToPage(currentPage.value - 1);
</script>

Pattern Provider/Consumer avec Slots

Créez un système de contexte utilisant provide/inject avec des slots :

<!-- ThemeProvider.vue -->
<template>
  <slot 
    :theme="theme" 
    :toggleTheme="toggleTheme"
    :isDark="isDark"
  ></slot>
</template>

<script setup>
import { ref, computed, provide } from 'vue';

const theme = ref('light');
const isDark = computed(() => theme.value === 'dark');

const toggleTheme = () => {
  theme.value = isDark.value ? 'light' : 'dark';
};

// Fournir le thème aux descendants
provide('theme', { theme, toggleTheme, isDark });
</script>

<!-- Utilisation -->
<template>
  <ThemeProvider v-slot="{ theme, toggleTheme }">
    <div :class="`theme-${theme}`">
      <button @click="toggleTheme">
        Changer de thème
      </button>
      <MainContent />
    </div>
  </ThemeProvider>
</template>

Slots avec Validation

Implémentez une validation de contenu de slot :

<!-- ValidatedSlot.vue -->
<template>
  <div>
    <slot v-if="isValid" :validate="validate"></slot>
    <div v-else class="error">
      {{ errorMessage }}
    </div>
  </div>
</template>

<script setup>
import { ref, watch } from 'vue';

const props = defineProps({
  rules: Array,
  value: [String, Number]
});

const isValid = ref(true);
const errorMessage = ref('');

const validate = () => {
  for (const rule of props.rules) {
    const result = rule(props.value);
    if (result !== true) {
      isValid.value = false;
      errorMessage.value = result;
      return false;
    }
  }
  isValid.value = true;
  errorMessage.value = '';
  return true;
};

watch(() => props.value, validate, { immediate: true });
</script>

Cas d'Usage Avancés

Liste Virtualisée avec Slots

<!-- VirtualList.vue -->
<template>
  <div class="virtual-list" @scroll="handleScroll">
    <div :style="{ height: `${totalHeight}px` }">
      <div 
        v-for="item in visibleItems" 
        :key="item.id"
        :style="{ transform: `translateY(${item.offsetTop}px)` }"
        class="virtual-item"
      >
        <slot 
          :item="item.data"
          :index="item.index"
        ></slot>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, computed } from 'vue';

const props = defineProps({
  items: Array,
  itemHeight: { type: Number, default: 50 },
  buffer: { type: Number, default: 3 }
});

const scrollTop = ref(0);

const totalHeight = computed(() => 
  props.items.length * props.itemHeight
);

const visibleItems = computed(() => {
  const start = Math.max(0, Math.floor(scrollTop.value / props.itemHeight) - props.buffer);
  const end = Math.ceil((scrollTop.value + window.innerHeight) / props.itemHeight) + props.buffer;
  
  return props.items.slice(start, end).map((data, i) => ({
    id: data.id,
    data,
    index: start + i,
    offsetTop: (start + i) * props.itemHeight
  }));
});

const handleScroll = (e) => {
  scrollTop.value = e.target.scrollTop;
};
</script>

Constructeur de Formulaire Dynamique

<!-- FormBuilder.vue -->
<template>
  <form @submit.prevent="handleSubmit">
    <div v-for="field in fields" :key="field.name">
      <slot 
        :name="`field-${field.type}`"
        :field="field"
        :value="formData[field.name]"
        :updateValue="(val) => updateField(field.name, val)"
        :error="errors[field.name]"
      >
        <!-- Fallback pour les champs non personnalisés -->
        <label>{{ field.label }}</label>
        <input 
          :type="field.type"
          :value="formData[field.name]"
          @input="updateField(field.name, $event.target.value)"
        />
      </slot>
    </div>
    
    <slot name="actions" :submit="handleSubmit" :reset="resetForm">
      <button type="submit">Soumettre</button>
      <button type="button" @click="resetForm">Réinitialiser</button>
    </slot>
  </form>
</template>

<script setup>
import { ref, reactive } from 'vue';

const props = defineProps({
  fields: Array,
  initialValues: Object
});

const emit = defineEmits(['submit']);

const formData = reactive({ ...props.initialValues });
const errors = ref({});

const updateField = (name, value) => {
  formData[name] = value;
  // Effacer l'erreur lors de la modification
  delete errors.value[name];
};

const handleSubmit = () => {
  emit('submit', formData);
};

const resetForm = () => {
  Object.assign(formData, props.initialValues);
  errors.value = {};
};
</script>

<!-- Utilisation -->
<template>
  <FormBuilder :fields="fields" :initialValues="initialValues">
    <template #field-email="{ field, value, updateValue, error }">
      <div class="custom-field">
        <label>{{ field.label }}</label>
        <input 
          type="email"
          :value="value"
          @input="updateValue($event.target.value)"
          :class="{ error: error }"
        />
        <span v-if="error" class="error-message">{{ error }}</span>
      </div>
    </template>
    
    <template #actions="{ submit, reset }">
      <div class="button-group">
        <button @click="submit" class="primary">Enregistrer</button>
        <button @click="reset" class="secondary">Annuler</button>
      </div>
    </template>
  </FormBuilder>
</template>

Debugging des Slots

Techniques de débogage

<template>
  <div>
    <!-- Afficher les slots disponibles -->
    <div v-if="isDev">
      Slots disponibles : {{ Object.keys($slots).join(', ') }}
    </div>
    
    <!-- Vérifier si un slot a du contenu -->
    <div v-if="$slots.default">
      <slot></slot>
    </div>
    <div v-else>
      Aucun contenu fourni
    </div>
  </div>
</template>

<script setup>
import { useSlots } from 'vue';

const slots = useSlots();
const isDev = import.meta.env.DEV;

// Logger les slots en développement
if (isDev) {
  console.log('Slots reçus:', Object.keys(slots));
  
  // Vérifier si un slot spécifique a été passé
  if (slots.header) {
    console.log('Le slot header est présent');
  }
}
</script>

Optimisations Avancées

Lazy Loading des Slots

<!-- LazySlot.vue -->
<template>
  <div>
    <div v-if="isVisible">
      <slot :load="load" :isLoaded="isLoaded"></slot>
    </div>
    <div v-else>
      <button @click="makeVisible">Charger le contenu</button>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue';

const isVisible = ref(false);
const isLoaded = ref(false);

const makeVisible = () => {
  isVisible.value = true;
};

const load = async () => {
  // Logique de chargement
  isLoaded.value = true;
};
</script>

Slots avec Transitions

<!-- AnimatedSlot.vue -->
<template>
  <TransitionGroup name="slot-fade" tag="div">
    <div v-for="item in items" :key="item.id">
      <slot 
        :item="item"
        :remove="() => removeItem(item.id)"
      ></slot>
    </div>
  </TransitionGroup>
</template>

<style scoped>
.slot-fade-enter-active,
.slot-fade-leave-active {
  transition: all 0.3s ease;
}

.slot-fade-enter-from {
  opacity: 0;
  transform: translateX(-30px);
}

.slot-fade-leave-to {
  opacity: 0;
  transform: translateX(30px);
}
</style>

Conclusion

Les slots sont un outil essentiel pour créer des composants Vue flexibles et réutilisables. En maîtrisant les slots de base, les slots nommés, les scoped slots et les patterns avancés, vous pouvez construire des composants puissants qui s'adaptent à de nombreux cas d'usage tout en maintenant une API propre et intuitive.

La compréhension de quand utiliser les slots plutôt que les props, comment composer efficacement les slots, et comment optimiser leurs performances vous aidera à créer de meilleures bibliothèques de composants et applications. N'oubliez jamais de documenter clairement vos APIs de slots et de tester vos composants avec divers scénarios de contenu de slot.

Les slots représentent un paradigme de programmation puissant qui encourage la séparation des préoccupations, la réutilisabilité et la composition. Avec la pratique, vous développerez une intuition pour identifier quand les slots sont la bonne solution et comment les structurer pour une flexibilité maximale.


Lectures Complémentaires :