Vue <Teleport> is a built-in component that moves a piece of a component's template into a different DOM node, such as <body> or #modal-root. Even though the HTML moves elsewhere in the DOM, the teleported content stays logically connected to its parent Vue component.
This solves common CSS headaches with modals, dropdowns, and toast alerts. When a parent card uses overflow: hidden, transform, or a low z-index, teleporting the modal to body lets it cover the full screen cleanly.
flowchart TD
Parent["ProfileCard.vue"] --> Teleport["Teleport to='body'"]
Teleport -->|Renders DOM Into| Body["Browser body Element"]
Parent -->|Keeps Props & Events| Teleport
<Teleport><body> so parent CSS stacking contexts never clip them.overflow: auto.In this example, isModalOpen and closeModal stay inside <script setup>, while <Teleport to="body"> places the overlay at the end of <body>.
<script setup>
import { ref } from 'vue';
const isModalOpen = ref(false);
function closeModal() {
isModalOpen.value = false;
}
</script>
<template>
<div class="card-container">
<h3>Account Settings</h3>
<button @click="isModalOpen = true">Change Password</button>
<Teleport to="body">
<div v-if="isModalOpen" class="modal-backdrop" @click.self="closeModal">
<div class="modal-content" role="dialog" aria-modal="true">
<h3>Update Password</h3>
<input type="password" placeholder="New password" />
<div class="actions">
<button @click="closeModal">Cancel</button>
<button @click="closeModal" class="primary">Save</button>
</div>
</div>
</div>
</Teleport>
</div>
</template>
<style scoped>
.modal-backdrop {
position: fixed;
inset: 0;
background: rgba(15, 23, 42, 0.6);
display: flex;
justify-content: center;
align-items: center;
z-index: 9999;
}
.modal-content {
background: #ffffff;
padding: 2rem;
border-radius: 8px;
min-width: 320px;
}
</style>
to="#modal-root" must already exist in the DOM before the component mounts. Otherwise, Vue logs a warning and cannot mount the teleported nodes..card-container .modal-content will not match. Style .modal-content directly or use :deep() only where appropriate.document.body.appendChild() in onMounted(), which breaks Vue's Virtual DOM tracking.inject() still work inside <Teleport>?Yes. <Teleport> only changes where the rendered HTML sits in the DOM. Logical parent-child relationships, props, emitted events, and provide/inject all work normally.
Yes. Pass :disabled="isInlineView" to <Teleport>. When disabled is true, Vue renders the content inline inside its original parent element.
<Teleport> components share the same target?Yes. If two components both use <Teleport to="#toasts">, Vue appends their rendered nodes to #toasts in the order they mount.
Sign in to track your learning journey, earn industry-recognized certificates, and join our elite developer community.
Quick Access With