Slots básicos, named y scoped

Intermedio
Vuejs
Vuejs
Actualizado: 27/03/2026

Qué son los slots

Los slots son un mecanismo de Vue que permite a un componente padre inyectar contenido dentro de un componente hijo. En lugar de que el hijo defina todo su contenido internamente, deja "huecos" (slots) donde el padre puede insertar plantillas HTML, texto o incluso otros componentes. Este patrón se conoce como distribución de contenido (content distribution).

Los slots son fundamentales para crear componentes reutilizables y flexibles: el componente hijo define la estructura y el comportamiento, mientras que el padre controla qué contenido se muestra.

Slots básicos

Un slot básico se define con la etiqueta <slot> dentro del template del componente hijo. El padre puede colocar cualquier contenido entre las etiquetas de apertura y cierre del componente:

<!-- AlertBox.vue -->
<template>
  <div class="alert-box">
    <strong>Atención: </strong>
    <slot></slot>
  </div>
</template>

<script setup lang="ts">
</script>

<style scoped>
.alert-box {
  padding: 1rem;
  background: #fff3cd;
  border: 1px solid #ffc107;
  border-radius: 4px;
}
</style>

Uso desde el padre:

<template>
  <AlertBox>
    Este es un mensaje de advertencia personalizado.
  </AlertBox>

  <AlertBox>
    <p>También se puede insertar <strong>HTML complejo</strong> dentro del slot.</p>
  </AlertBox>
</template>

<script setup lang="ts">
import AlertBox from './AlertBox.vue'
</script>

Todo lo que el padre coloque entre <AlertBox> y </AlertBox> reemplaza la etiqueta <slot> en el hijo.

Contenido por defecto (fallback)

Cuando el padre no proporciona contenido para un slot, se puede definir un contenido por defecto colocándolo entre las etiquetas <slot>:

<!-- SubmitButton.vue -->
<template>
  <button class="btn" type="submit">
    <slot>Enviar</slot>
  </button>
</template>

<script setup lang="ts">
</script>
<template>
  <!-- Muestra "Enviar" (contenido por defecto) -->
  <SubmitButton />

  <!-- Muestra "Guardar cambios" (contenido del padre) -->
  <SubmitButton>Guardar cambios</SubmitButton>
</template>

Named slots (slots con nombre)

Cuando un componente necesita múltiples slots, se utilizan named slots para distinguirlos. Cada slot se identifica con un atributo name. El slot sin nombre se denomina default:

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

    <main>
      <slot></slot>
    </main>

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

<script setup lang="ts">
</script>

<style scoped>
.page-layout {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
}

header {
  background: #343a40;
  color: white;
  padding: 1rem;
}

main {
  flex: 1;
  padding: 2rem;
}

footer {
  background: #f8f9fa;
  padding: 1rem;
  text-align: center;
}
</style>

Desde el padre, se usa <template #nombre> (abreviatura de <template v-slot:nombre>) para enviar contenido a cada slot:

<template>
  <PageLayout>
    <template #header>
      <h1>Mi aplicación</h1>
      <nav>
        <a href="/">Inicio</a>
        <a href="/about">Acerca de</a>
      </nav>
    </template>

    <template #default>
      <p>Este es el contenido principal de la página.</p>
      <p>Va al slot por defecto (sin nombre).</p>
    </template>

    <template #footer>
      <p>&copy; Mi aplicación</p>
    </template>
  </PageLayout>
</template>

<script setup lang="ts">
import PageLayout from './PageLayout.vue'
</script>

La sintaxis #header es la forma abreviada de v-slot:header. Ambas son equivalentes. El contenido que no está dentro de ningún <template> con nombre va automáticamente al slot default.

Scoped slots

Los scoped slots permiten que el componente hijo pase datos al padre a través del slot. El hijo expone propiedades en la etiqueta <slot> y el padre las recibe como parámetros desestructurados:

<!-- UserList.vue -->
<template>
  <ul>
    <li v-for="(user, index) in users" :key="user.id">
      <slot :user="user" :index="index"></slot>
    </li>
  </ul>
</template>

<script setup lang="ts">
interface User {
  id: number
  name: string
  role: string
}

defineProps<{
  users: User[]
}>()
</script>

El padre recibe los datos del slot mediante la desestructuración de las slot props:

<template>
  <UserList :users="usuarios">
    <template #default="{ user, index }">
      <span>{{ index + 1 }}. {{ user.name }} - {{ user.role }}</span>
    </template>
  </UserList>
</template>

<script setup lang="ts">
import UserList from './UserList.vue'

const usuarios = [
  { id: 1, name: 'Ana Garcia', role: 'Admin' },
  { id: 2, name: 'Carlos Lopez', role: 'Editor' },
  { id: 3, name: 'Maria Torres', role: 'Lector' }
]
</script>

También se puede combinar con named slots. El hijo puede tener varios scoped slots con diferentes datos:

<!-- DataTable.vue -->
<template>
  <table>
    <thead>
      <tr>
        <th v-for="col in columns" :key="col.key">
          <slot name="header" :column="col">
            {{ col.label }}
          </slot>
        </th>
      </tr>
    </thead>
    <tbody>
      <tr v-for="item in items" :key="item.id">
        <td v-for="col in columns" :key="col.key">
          <slot name="cell" :item="item" :column="col" :value="item[col.key]">
            {{ item[col.key] }}
          </slot>
        </td>
      </tr>
    </tbody>
  </table>
</template>

<script setup lang="ts">
interface Column {
  key: string
  label: string
}

defineProps<{
  items: Record<string, any>[]
  columns: Column[]
}>()
</script>

Uso desde el padre, personalizando la celda de una columna concreta:

<template>
  <DataTable :items="productos" :columns="columnas">
    <template #cell="{ item, column, value }">
      <span v-if="column.key === 'price'" class="precio">{{ value }} EUR</span>
      <span v-else-if="column.key === 'stock'" :class="value > 0 ? 'disponible' : 'agotado'">
        {{ value > 0 ? 'Disponible' : 'Agotado' }}
      </span>
      <span v-else>{{ value }}</span>
    </template>
  </DataTable>
</template>

<script setup lang="ts">
import DataTable from './DataTable.vue'

const columnas = [
  { key: 'name', label: 'Producto' },
  { key: 'price', label: 'Precio' },
  { key: 'stock', label: 'Stock' }
]

const productos = [
  { id: 1, name: 'Camiseta', price: 19.99, stock: 50 },
  { id: 2, name: 'Pantalon', price: 39.99, stock: 0 },
  { id: 3, name: 'Zapatos', price: 59.99, stock: 12 }
]
</script>

Nombres de slot dinámicos

Vue permite usar nombres de slot dinámicos mediante la sintaxis de corchetes #[variable]:

<template>
  <PageLayout>
    <template #[slotActivo]>
      <p>Este contenido aparece en el slot: {{ slotActivo }}</p>
    </template>
  </PageLayout>

  <button @click="slotActivo = 'header'">Mostrar en header</button>
  <button @click="slotActivo = 'footer'">Mostrar en footer</button>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import PageLayout from './PageLayout.vue'

const slotActivo = ref('header')
</script>

Esto permite cambiar dinámicamente dónde se inyecta el contenido en función del estado de la aplicación.

Comprobar si un slot tiene contenido

El objeto $slots expone los slots que el padre ha proporcionado. Se puede utilizar para mostrar condicionalmente una sección solo si el padre la ha rellenado:

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

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

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

<script setup lang="ts">
</script>

<style scoped>
.card {
  border: 1px solid #dee2e6;
  border-radius: 8px;
  overflow: hidden;
}

.card-header {
  background: #f8f9fa;
  padding: 1rem;
  border-bottom: 1px solid #dee2e6;
  font-weight: bold;
}

.card-body {
  padding: 1rem;
}

.card-footer {
  background: #f8f9fa;
  padding: 0.75rem 1rem;
  border-top: 1px solid #dee2e6;
}
</style>

De esta forma, si el padre no pasa un slot header, la sección del encabezado no se renderiza en absoluto, manteniendo el componente limpio.

Patrones prácticos

Componente renderless

Un componente renderless no renderiza HTML propio, solo lógica. Expone datos y funciones a través de un scoped slot:

<!-- FetchData.vue -->
<template>
  <slot :data="data" :loading="loading" :error="error"></slot>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue'

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

const data = ref<any>(null)
const loading = ref(true)
const error = ref<string | null>(null)

onMounted(async () => {
  try {
    const response = await fetch(props.url)
    data.value = await response.json()
  } catch (e) {
    error.value = (e as Error).message
  } finally {
    loading.value = false
  }
})
</script>

Uso:

<template>
  <FetchData url="/api/users">
    <template #default="{ data, loading, error }">
      <p v-if="loading">Cargando...</p>
      <p v-else-if="error" class="error">{{ error }}</p>
      <ul v-else>
        <li v-for="user in data" :key="user.id">{{ user.name }}</li>
      </ul>
    </template>
  </FetchData>
</template>

Layout componente con multiples slots

El patron de header, default, footer es uno de los mas utilizados para crear componentes de layout reutilizables que mantienen una estructura visual consistente pero permiten personalizar cada seccion:

<template>
  <Modal>
    <template #header>
      <h2>Confirmar accion</h2>
    </template>

    <p>¿Estas seguro de que quieres eliminar este elemento?</p>

    <template #footer>
      <button @click="cancelar">Cancelar</button>
      <button @click="confirmar" class="btn-danger">Eliminar</button>
    </template>
  </Modal>
</template>

Los slots son una herramienta clave para construir librerias de componentes reutilizables en Vue, ya que permiten separar la estructura del contenido y dar al consumidor del componente control total sobre la presentacion de los datos.

Alan Sastre - Autor del tutorial

Alan Sastre

Ingeniero de Software y formador, CEO en CertiDevs

Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, Vuejs es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.

Más tutoriales de Vuejs

Explora más contenido relacionado con Vuejs y continúa aprendiendo con nuestros tutoriales gratuitos.

Aprendizajes de esta lección

Cursos que incluyen esta lección

Esta lección forma parte de los siguientes cursos estructurados con rutas de aprendizaje