00
:
00
:
00
:
00
Corso SEO AI - Usa SEOEMAIL al checkout per il 30% di sconto

Slot

Cos’e uno slot

Gli slot permettono a un componente genitore di iniettare contenuto all’interno del template di un componente figlio. Sono il meccanismo principale per la composizione dei componenti in Vue.

Slot default

Lo slot base (senza nome) accetta qualsiasi contenuto passato tra i tag del componente.

<!-- BaseCard.vue -->
<script setup lang="ts">
defineProps<{
  titolo: string
}>()
</script>

<template>
  <div class="card">
    <div class="card-header">
      <h3>{{ titolo }}</h3>
    </div>
    <div class="card-body">
      <!-- Lo slot renderizza il contenuto passato dal genitore -->
      <slot />
    </div>
  </div>
</template>

<style scoped>
.card {
  border: 1px solid #e2e8f0;
  border-radius: 8px;
  overflow: hidden;
}
.card-header {
  background: #f7fafc;
  padding: 1rem;
  border-bottom: 1px solid #e2e8f0;
}
.card-body {
  padding: 1rem;
}
</style>
<!-- Utilizzo -->
<template>
  <BaseCard titolo="Profilo utente">
    <!-- Questo contenuto viene inserito nello slot -->
    <p>Nome: Mario Rossi</p>
    <p>Email: mario@example.com</p>
    <button>Modifica profilo</button>
  </BaseCard>

  <BaseCard titolo="Statistiche">
    <ul>
      <li>Visite: 1234</li>
      <li>Commenti: 56</li>
    </ul>
  </BaseCard>
</template>

Slot fallback

Lo slot puo avere un contenuto predefinito che viene mostrato quando il genitore non fornisce contenuto.

<!-- BaseButton.vue -->
<script setup lang="ts">
defineProps<{
  variante?: 'primario' | 'secondario' | 'pericolo'
}>()
</script>

<template>
  <button :class="['btn', `btn-${variante || 'primario'}`]">
    <!-- Fallback: mostrato se non viene passato contenuto -->
    <slot>Clicca qui</slot>
  </button>
</template>
<!-- Utilizzo -->
<template>
  <!-- Usa il fallback "Clicca qui" -->
  <BaseButton />

  <!-- Sovrascrive con contenuto personalizzato -->
  <BaseButton variante="pericolo">
    Elimina account
  </BaseButton>

  <!-- Contenuto complesso -->
  <BaseButton>
    <span class="icona">🔍</span>
    Cerca
  </BaseButton>
</template>

Slot con nome (named slots)

Quando un componente ha piu aree di contenuto, usa slot con nome per distinguerle.

<!-- LayoutPagina.vue -->
<template>
  <div class="layout">
    <header>
      <slot name="header" />
    </header>

    <aside>
      <slot name="sidebar" />
    </aside>

    <main>
      <!-- Lo slot senza nome e lo slot "default" -->
      <slot />
    </main>

    <footer>
      <slot name="footer" />
    </footer>
  </div>
</template>

<style scoped>
.layout {
  display: grid;
  grid-template-areas:
    "header header"
    "sidebar main"
    "footer footer";
  grid-template-columns: 250px 1fr;
  min-height: 100vh;
}
header { grid-area: header; }
aside { grid-area: sidebar; }
main { grid-area: main; }
footer { grid-area: footer; }
</style>
<!-- Utilizzo con v-slot (abbreviato #) -->
<template>
  <LayoutPagina>
    <template #header>
      <nav>
        <a href="/">Home</a>
        <a href="/prodotti">Prodotti</a>
      </nav>
    </template>

    <template #sidebar>
      <ul>
        <li>Categoria 1</li>
        <li>Categoria 2</li>
        <li>Categoria 3</li>
      </ul>
    </template>

    <!-- Contenuto default (senza #) -->
    <h1>Benvenuto</h1>
    <p>Contenuto principale della pagina.</p>

    <template #footer>
      <p>&copy; 2026 Mio Sito</p>
    </template>
  </LayoutPagina>
</template>

Slot condizionali

Puoi verificare se uno slot ha contenuto con $slots.

<!-- BaseCard.vue migliorato -->
<script setup lang="ts">
import { useSlots } from 'vue'

const slots = useSlots()
const haHeader = !!slots.header
const haFooter = !!slots.footer
</script>

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

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

    <div v-if="haFooter" class="card-footer">
      <slot name="footer" />
    </div>
  </div>
</template>

Scoped Slot

Gli scoped slot permettono al componente figlio di passare dati al contenuto dello slot. Il figlio “espone” dati che il genitore puo utilizzare nel template dello slot.

Esempio base

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

interface Elemento {
  id: number
  nome: string
  completato: boolean
}

const props = defineProps<{
  elementi: Elemento[]
}>()

const emit = defineEmits<{
  toggle: [id: number]
  rimuovi: [id: number]
}>()
</script>

<template>
  <ul class="lista">
    <li v-for="(elemento, indice) in elementi" :key="elemento.id">
      <!-- Passa dati al genitore tramite lo slot -->
      <slot
        :elemento="elemento"
        :indice="indice"
        :toggle="() => emit('toggle', elemento.id)"
        :rimuovi="() => emit('rimuovi', elemento.id)"
      />
    </li>
  </ul>
</template>
<!-- Utilizzo: il genitore decide come renderizzare ogni elemento -->
<script setup lang="ts">
import { ref } from 'vue'
import ListaElementi from './ListaElementi.vue'

const compiti = ref([
  { id: 1, nome: 'Fare la spesa', completato: false },
  { id: 2, nome: 'Lavare i piatti', completato: true },
  { id: 3, nome: 'Studiare Vue', completato: false }
])

function toggleCompito(id: number) {
  const compito = compiti.value.find(c => c.id === id)
  if (compito) compito.completato = !compito.completato
}

function rimuoviCompito(id: number) {
  compiti.value = compiti.value.filter(c => c.id !== id)
}
</script>

<template>
  <!-- Visualizzazione semplice -->
  <ListaElementi
    :elementi="compiti"
    @toggle="toggleCompito"
    @rimuovi="rimuoviCompito"
  >
    <template #default="{ elemento, toggle, rimuovi }">
      <span
        :style="{ textDecoration: elemento.completato ? 'line-through' : 'none' }"
        @click="toggle"
      >
        {{ elemento.nome }}
      </span>
      <button @click="rimuovi">X</button>
    </template>
  </ListaElementi>
</template>

Scoped slot con destrutturazione

<!-- Destrutturazione diretta delle props dello slot -->
<template>
  <ListaElementi :elementi="compiti">
    <!-- Destrutturazione con rinomina -->
    <template #default="{ elemento: compito, indice: i }">
      <span>{{ i + 1 }}. {{ compito.nome }}</span>
    </template>
  </ListaElementi>
</template>

Scoped slot con nome

<!-- TabellaAvanzata.vue -->
<script setup lang="ts">
interface Colonna {
  chiave: string
  etichetta: string
}

defineProps<{
  colonne: Colonna[]
  righe: Record<string, any>[]
}>()
</script>

<template>
  <table>
    <thead>
      <tr>
        <th v-for="col in colonne" :key="col.chiave">
          <!-- Slot per personalizzare l'header -->
          <slot :name="`header-${col.chiave}`" :colonna="col">
            {{ col.etichetta }}
          </slot>
        </th>
      </tr>
    </thead>
    <tbody>
      <tr v-for="(riga, indice) in righe" :key="indice">
        <td v-for="col in colonne" :key="col.chiave">
          <!-- Slot per personalizzare la cella -->
          <slot :name="`cella-${col.chiave}`" :valore="riga[col.chiave]" :riga="riga">
            {{ riga[col.chiave] }}
          </slot>
        </td>
      </tr>
    </tbody>
  </table>
</template>
<!-- Utilizzo della tabella personalizzabile -->
<script setup>
const colonne = [
  { chiave: 'nome', etichetta: 'Nome' },
  { chiave: 'email', etichetta: 'Email' },
  { chiave: 'stato', etichetta: 'Stato' }
]

const utenti = [
  { nome: 'Mario', email: 'mario@test.com', stato: 'attivo' },
  { nome: 'Luigi', email: 'luigi@test.com', stato: 'inattivo' }
]
</script>

<template>
  <TabellaAvanzata :colonne="colonne" :righe="utenti">
    <!-- Personalizza come viene renderizzata la colonna "stato" -->
    <template #cella-stato="{ valore }">
      <span :class="['badge', valore]">
        {{ valore === 'attivo' ? 'Attivo' : 'Inattivo' }}
      </span>
    </template>

    <!-- Personalizza l'header della colonna "nome" -->
    <template #header-nome>
      <strong>Nome Utente</strong>
    </template>
  </TabellaAvanzata>
</template>

Pattern di composizione con slot

Renderless Component

Un componente senza template proprio che fornisce solo logica tramite scoped slot.

<!-- FetchDati.vue - Componente renderless -->
<script setup lang="ts">
import { ref, onMounted, watch } from 'vue'

const props = defineProps<{
  url: string
}>()

const dati = ref<any>(null)
const errore = ref<string | null>(null)
const isCaricamento = ref(true)

async function carica() {
  isCaricamento.value = true
  errore.value = null
  try {
    const risposta = await fetch(props.url)
    if (!risposta.ok) throw new Error(`HTTP ${risposta.status}`)
    dati.value = await risposta.json()
  } catch (e) {
    errore.value = (e as Error).message
  } finally {
    isCaricamento.value = false
  }
}

onMounted(carica)
watch(() => props.url, carica)
</script>

<template>
  <slot
    :dati="dati"
    :errore="errore"
    :isCaricamento="isCaricamento"
    :ricarica="carica"
  />
</template>
<!-- Utilizzo -->
<template>
  <FetchDati url="/api/utenti">
    <template #default="{ dati, errore, isCaricamento, ricarica }">
      <div v-if="isCaricamento">Caricamento...</div>
      <div v-else-if="errore">
        Errore: {{ errore }}
        <button @click="ricarica">Riprova</button>
      </div>
      <ul v-else>
        <li v-for="utente in dati" :key="utente.id">
          {{ utente.nome }}
        </li>
      </ul>
    </template>
  </FetchDati>
</template>

Pattern contenitore/contenuto

<!-- Accordion.vue -->
<script setup lang="ts">
import { ref, provide } from 'vue'

const elementoAperto = ref<string | null>(null)

function toggle(id: string) {
  elementoAperto.value = elementoAperto.value === id ? null : id
}

provide('accordion', { elementoAperto, toggle })
</script>

<template>
  <div class="accordion">
    <slot />
  </div>
</template>
<!-- AccordionItem.vue -->
<script setup lang="ts">
import { inject, computed } from 'vue'

const props = defineProps<{
  id: string
  titolo: string
}>()

const { elementoAperto, toggle } = inject('accordion') as any

const isAperto = computed(() => elementoAperto.value === props.id)
</script>

<template>
  <div class="accordion-item">
    <button @click="toggle(id)" class="accordion-header">
      {{ titolo }}
      <span>{{ isAperto ? '−' : '+' }}</span>
    </button>
    <div v-show="isAperto" class="accordion-body">
      <slot />
    </div>
  </div>
</template>
<!-- Utilizzo -->
<template>
  <Accordion>
    <AccordionItem id="1" titolo="Sezione 1">
      <p>Contenuto della prima sezione.</p>
    </AccordionItem>
    <AccordionItem id="2" titolo="Sezione 2">
      <p>Contenuto della seconda sezione.</p>
    </AccordionItem>
    <AccordionItem id="3" titolo="Sezione 3">
      <p>Contenuto della terza sezione.</p>
    </AccordionItem>
  </Accordion>
</template>

Best practice

  • Usa slot per la composizione: Preferisci slot rispetto a props complesse per il contenuto
  • Fornisci fallback significativi: Definisci sempre un contenuto predefinito utile
  • Scoped slot per la personalizzazione: Quando il figlio ha dati che il genitore deve visualizzare
  • Usa nomi descrittivi: #header, #footer, #azioni sono meglio di #slot1, #slot2
  • Verifica l’esistenza dello slot: Usa useSlots() per rendering condizionale
  • Preferisci composables ai renderless components: Nella maggior parte dei casi i composables sono piu semplici