Teleport

Intermedio
Vuejs
Vuejs
Actualizado: 27/03/2026

Qué es Teleport

Teleport es un componente built-in de Vue que permite renderizar el contenido de un componente en un nodo del DOM diferente al que ocupa en la jerarquía de componentes. Esto resuelve un problema común: cuando un componente necesita mostrarse visualmente fuera de su contenedor padre (por ejemplo, un modal que debe cubrir toda la pantalla), pero lógicamente pertenece al componente que lo controla.

Sin Teleport, un modal definido dentro de un componente profundamente anidado puede verse afectado por los estilos CSS del padre (como overflow: hidden, z-index o transform), causando problemas de posicionamiento. Teleport mueve el DOM renderizado al destino especificado mientras mantiene la relación padre-hijo en la jerarquía de Vue, lo que significa que props, emits y provide/inject siguen funcionando normalmente.

Uso básico con la prop to

La prop to indica el selector CSS o elemento del DOM donde se renderizará el contenido:

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

const showNotification = ref(false)
</script>

<template>
  <button @click="showNotification = true">Mostrar notificación</button>

  <Teleport to="body">
    <div v-if="showNotification" class="notification">
      <p>Operación completada correctamente</p>
      <button @click="showNotification = false">Cerrar</button>
    </div>
  </Teleport>
</template>

<style scoped>
.notification {
  position: fixed;
  top: 20px;
  right: 20px;
  padding: 16px 24px;
  background: #2ecc71;
  color: white;
  border-radius: 8px;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
  z-index: 1000;
}
</style>

La prop to acepta:

  • Un selector CSS como "body", "#modals" o ".notification-container"
  • Una referencia directa a un elemento DOM

Es habitual crear un contenedor dedicado en index.html:

<body>
  <div id="app"></div>
  <div id="modals"></div>
  <div id="notifications"></div>
</body>

Caso de uso: modal completo

Un modal es el caso de uso más común de Teleport. A continuación, un componente modal reutilizable:

<!-- BaseModal.vue -->
<script setup lang="ts">
defineProps<{
  title: string
  show: boolean
}>()

const emit = defineEmits<{
  close: []
}>()

function handleOverlayClick(event: MouseEvent) {
  if (event.target === event.currentTarget) {
    emit('close')
  }
}
</script>

<template>
  <Teleport to="#modals">
    <div v-if="show" class="modal-overlay" @click="handleOverlayClick">
      <div class="modal-container" role="dialog" aria-modal="true">
        <header class="modal-header">
          <h2>{{ title }}</h2>
          <button class="modal-close" @click="emit('close')">&times;</button>
        </header>
        <div class="modal-body">
          <slot />
        </div>
        <footer v-if="$slots.footer" class="modal-footer">
          <slot name="footer" />
        </footer>
      </div>
    </div>
  </Teleport>
</template>

<style scoped>
.modal-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.5);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 1000;
}

.modal-container {
  background: white;
  border-radius: 12px;
  padding: 24px;
  min-width: 400px;
  max-width: 90vw;
  max-height: 90vh;
  overflow-y: auto;
  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
}

.modal-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 16px;
}

.modal-close {
  background: none;
  border: none;
  font-size: 24px;
  cursor: pointer;
  padding: 4px 8px;
}

.modal-footer {
  margin-top: 16px;
  display: flex;
  justify-content: flex-end;
  gap: 8px;
}
</style>

Uso desde un componente padre:

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

const showConfirm = ref(false)

function handleConfirm() {
  console.log('Acción confirmada')
  showConfirm.value = false
}
</script>

<template>
  <button @click="showConfirm = true">Eliminar elemento</button>

  <BaseModal
    title="Confirmar eliminación"
    :show="showConfirm"
    @close="showConfirm = false"
  >
    <p>¿Estás seguro de que deseas eliminar este elemento? Esta acción no se puede deshacer.</p>
    <template #footer>
      <button @click="showConfirm = false">Cancelar</button>
      <button @click="handleConfirm" class="btn-danger">Eliminar</button>
    </template>
  </BaseModal>
</template>

Teleport condicional con disabled

La prop disabled permite desactivar el teletransporte condicionalmente. Cuando disabled es true, el contenido se renderiza en su posición original dentro del componente padre:

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

const isMobile = ref(window.innerWidth < 768)

window.addEventListener('resize', () => {
  isMobile.value = window.innerWidth < 768
})
</script>

<template>
  <Teleport to="#sidebar" :disabled="isMobile">
    <nav class="navigation">
      <ul>
        <li><a href="/inicio">Inicio</a></li>
        <li><a href="/productos">Productos</a></li>
        <li><a href="/contacto">Contacto</a></li>
      </ul>
    </nav>
  </Teleport>
</template>

En este ejemplo, la navegación se teletransporta a un sidebar en pantallas grandes, pero se renderiza inline en móviles. Esto es útil para layouts responsivos donde el mismo contenido debe aparecer en diferentes posiciones del DOM según el viewport.

Teleport diferido con defer

La prop defer hace que el Teleport espere hasta que el resto del DOM de la misma actualización se haya renderizado antes de resolver el destino. Esto es necesario cuando el contenedor destino se crea en el mismo componente o en la misma actualización del DOM:

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

const showTooltip = ref(false)
</script>

<template>
  <Teleport to="#tooltip-container" defer>
    <div v-if="showTooltip" class="tooltip">
      Este es un tooltip teletransportado
    </div>
  </Teleport>

  <!-- El contenedor se renderiza después del Teleport en el template -->
  <div id="tooltip-container"></div>
</template>

Sin defer, el Teleport intentaría montar el contenido antes de que el contenedor #tooltip-container existiera, causando un warning. Con defer, Vue espera a que todos los elementos del template actual se monten antes de resolver el destino del Teleport.

El prop defer es útil cuando:

  • El contenedor destino se crea en el mismo componente que el Teleport
  • Múltiples componentes se montan simultáneamente y uno depende del otro
  • El contenedor está dentro de un v-if que se resuelve en el mismo tick

Anidar Teleport dentro de Transition

Se puede combinar Teleport con Transition para animar la entrada y salida del contenido teletransportado:

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

const showPanel = ref(false)
</script>

<template>
  <button @click="showPanel = !showPanel">Toggle panel</button>

  <Teleport to="body">
    <Transition name="slide-fade">
      <div v-if="showPanel" class="side-panel">
        <h3>Panel lateral</h3>
        <p>Contenido del panel teletransportado al body</p>
        <button @click="showPanel = false">Cerrar</button>
      </div>
    </Transition>
  </Teleport>
</template>

<style>
.side-panel {
  position: fixed;
  top: 0;
  right: 0;
  width: 350px;
  height: 100vh;
  background: white;
  box-shadow: -4px 0 20px rgba(0, 0, 0, 0.1);
  padding: 24px;
  z-index: 1000;
}

.slide-fade-enter-active {
  transition: all 0.3s ease-out;
}

.slide-fade-leave-active {
  transition: all 0.2s ease-in;
}

.slide-fade-enter-from {
  transform: translateX(100%);
  opacity: 0;
}

.slide-fade-leave-to {
  transform: translateX(100%);
  opacity: 0;
}
</style>

El Transition debe estar dentro del Teleport, no al revés. Esto garantiza que la animación se aplique al contenido una vez teletransportado al destino.

Múltiples Teleports al mismo destino

Varios componentes Teleport pueden enviar contenido al mismo destino. Los contenidos se añaden en el orden en que aparecen:

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

const notifications = ref([
  { id: 1, message: 'Archivo guardado', type: 'success' as const },
  { id: 2, message: 'Conexión perdida', type: 'error' as const }
])
</script>

<template>
  <Teleport to="#notifications" v-for="notif in notifications" :key="notif.id">
    <div :class="['toast', `toast-${notif.type}`]">
      {{ notif.message }}
    </div>
  </Teleport>
</template>

Cada Teleport individual monta su contenido en el destino #notifications. Esto permite que diferentes componentes contribuyan notificaciones al mismo contenedor sin necesidad de un estado global.

Caso de uso: sistema de tooltips

<!-- Tooltip.vue -->
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount } from 'vue'

const props = defineProps<{
  text: string
  position?: 'top' | 'bottom' | 'left' | 'right'
}>()

const { position = 'top' } = props

const show = ref(false)
const triggerRef = ref<HTMLElement | null>(null)
const tooltipStyle = ref({ top: '0px', left: '0px' })

function updatePosition() {
  if (!triggerRef.value) return
  const rect = triggerRef.value.getBoundingClientRect()

  switch (position) {
    case 'top':
      tooltipStyle.value = {
        top: `${rect.top - 8}px`,
        left: `${rect.left + rect.width / 2}px`
      }
      break
    case 'bottom':
      tooltipStyle.value = {
        top: `${rect.bottom + 8}px`,
        left: `${rect.left + rect.width / 2}px`
      }
      break
  }
}

function showTooltip() {
  updatePosition()
  show.value = true
}

function hideTooltip() {
  show.value = false
}
</script>

<template>
  <span
    ref="triggerRef"
    @mouseenter="showTooltip"
    @mouseleave="hideTooltip"
  >
    <slot />
  </span>

  <Teleport to="body">
    <Transition name="tooltip">
      <div
        v-if="show"
        class="tooltip"
        :class="`tooltip-${position}`"
        :style="tooltipStyle"
      >
        {{ text }}
      </div>
    </Transition>
  </Teleport>
</template>

<style>
.tooltip {
  position: fixed;
  transform: translateX(-50%);
  background: #333;
  color: white;
  padding: 6px 12px;
  border-radius: 4px;
  font-size: 13px;
  white-space: nowrap;
  z-index: 9999;
  pointer-events: none;
}

.tooltip-enter-active, .tooltip-leave-active {
  transition: opacity 0.15s ease;
}

.tooltip-enter-from, .tooltip-leave-to {
  opacity: 0;
}
</style>

Teleport garantiza que el tooltip se renderice directamente en el body, evitando problemas con contenedores que tengan overflow: hidden o transform aplicado, que romperían el posicionamiento absoluto/fijo del tooltip.

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