Модальне вікно логічно належить компоненту, що його відкриває (кнопка, стан, обробники - поруч). Але візуально воно має бути поверх усієї сторінки. Якщо розмітка вікна лишається глибоко в DOM усередині батьків, CSS батьків ламає позиціонування:
transform,filter,perspectiveна предку роблятьposition: fixedвідносним до цього предка, а не до вікна;overflow: hiddenобрізає вікно;z-indexобмежений контекстом накладання (stacking context) батька.
<Teleport> рендерить вміст в інше місце DOM, лишаючи його логічно частиною компонента:
<script setup>
const open = ref(false);
</script>
<template>
<button @click="open = true">Видалити акаунт</button>
<Teleport to="body">
<div v-if="open" class="fixed inset-0 z-50 grid place-items-center bg-black/50">
<div role="dialog" aria-modal="true" class="rounded bg-white p-6">
<p>Ви впевнені?</p>
<button @click="open = false">Скасувати</button>
</div>
</div>
</Teleport>
</template>
Що зберігається: реактивність, props, події, provide/inject - вміст лишається дочірнім у дереві компонентів. Змінюється лише місце в DOM.
Параметри:
to- CSS-селектор чи елемент; ціль має існувати на момент монтування;:disabled="isMobile"- рендерити на місці (наприклад, на мобільних вікно стає звичайним блоком);defer(Vue 3.5+) - відкласти пошук цілі до кінця поточного монтування, щоб ціллю міг бути елемент, який Vue рендерить пізніше в тому самому дереві.
Кілька Teleport в одну ціль дописують вміст по черзі - зручно для стеку сповіщень.
Що ще треба для якісного модального вікна (Teleport цього не робить):
- фокус: перевести фокус у вікно при відкритті, втримувати його всередині (focus trap) і повертати на кнопку після закриття;
- закриття по Escape і кліку на підкладку;
- блокування прокрутки сторінки під вікном;
role="dialog",aria-modal,aria-labelledbyдля зчитувачів екрана.
Нативна альтернатива - елемент <dialog> з showModal(): браузер сам дає верхній шар (top layer, без проблем із z-index), фокус і Escape. У сучасних застосунках це часто простіше за Teleport.
SSR: телепортований вміст рендериться окремо - його треба вставити в HTML вручну, інакше буде розбіжність при гідратації.