Skip to content

Teleport во Vue 3

1. Что такое Teleport?

Teleport — встроенный компонент Vue 3, который позволяет "телепортировать" часть шаблона в другое место в DOM, за пределы родительского компонента. Это полезно для модальных окон, тултипов, уведомлений и других элементов, которые должны рендериться на верхнем уровне DOM.

2. Базовое использование

Модальное окно

vue
<script setup>
import { ref } from 'vue';

const isOpen = ref(false);
</script>

<template>
  <button @click="isOpen = true">Открыть модалку</button>

  <Teleport to="body">
    <div v-if="isOpen" class="modal-overlay">
      <div class="modal">
        <h2>Модальное окно</h2>
        <p>Контент модального окна</p>
        <button @click="isOpen = false">Закрыть</button>
      </div>
    </div>
  </Teleport>
</template>

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

.modal {
  background: white;
  padding: 2rem;
  border-radius: 8px;
  max-width: 500px;
  width: 90%;
}
</style>

3. Атрибут to

Атрибут to принимает CSS-селектор или ссылку на DOM-элемент.

vue
<!-- CSS-селектор -->
<Teleport to="#modals">
  <div>Телепортировано в #modals</div>
</Teleport>

<Teleport to=".notifications">
  <div>Телепортировано в .notifications</div>
</Teleport>

<Teleport to="body">
  <div>Телепортировано в body</div>
</Teleport>

Динамический target

vue
<script setup>
import { ref } from 'vue';

const target = ref('#container-a');
</script>

<template>
  <button @click="target = '#container-a'">Контейнер A</button>
  <button @click="target = '#container-b'">Контейнер B</button>

  <Teleport :to="target">
    <div>Динамический телепорт</div>
  </Teleport>

  <div id="container-a"></div>
  <div id="container-b"></div>
</template>

4. Отключение Teleport

Атрибут disabled позволяет условно отключать телепортацию.

vue
<script setup>
import { ref } from 'vue';

const isMobile = ref(window.innerWidth < 768);
</script>

<template>
  <!-- На мобильных рендерится на месте, на десктопе — в body -->
  <Teleport to="body" :disabled="isMobile">
    <div class="sidebar">
      Боковая панель
    </div>
  </Teleport>
</template>

5. Несколько Teleport в один target

Несколько компонентов Teleport могут рендериться в один и тот же целевой элемент. Порядок соответствует порядку появления в коде.

vue
<template>
  <Teleport to="#notifications">
    <div class="notification">Первое уведомление</div>
  </Teleport>

  <Teleport to="#notifications">
    <div class="notification">Второе уведомление</div>
  </Teleport>
</template>

Результат в DOM:

html
<div id="notifications">
  <div class="notification">Первое уведомление</div>
  <div class="notification">Второе уведомление</div>
</div>

6. Практические примеры

Система уведомлений (Toast)

vue
<!-- ToastNotification.vue -->
<script setup>
defineProps<{
  message: string;
  type: 'success' | 'error' | 'warning';
}>();

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

<template>
  <Teleport to="#toast-container">
    <div :class="['toast', `toast--${type}`]">
      <span>{{ message }}</span>
      <button @click="emit('close')">×</button>
    </div>
  </Teleport>
</template>

Тултип

vue
<!-- Tooltip.vue -->
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';

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

const triggerRef = ref<HTMLElement | null>(null);
const isVisible = ref(false);
const position = ref({ top: 0, left: 0 });

function show() {
  if (!triggerRef.value) return;
  const rect = triggerRef.value.getBoundingClientRect();
  position.value = {
    top: rect.top - 10,
    left: rect.left + rect.width / 2,
  };
  isVisible.value = true;
}

function hide() {
  isVisible.value = false;
}
</script>

<template>
  <span ref="triggerRef" @mouseenter="show" @mouseleave="hide">
    <slot />
  </span>

  <Teleport to="body">
    <div
      v-if="isVisible"
      class="tooltip"
      :style="{
        top: position.top + 'px',
        left: position.left + 'px',
      }"
    >
      {{ text }}
    </div>
  </Teleport>
</template>

Контекстное меню

vue
<script setup>
import { ref } from 'vue';

const isVisible = ref(false);
const menuPosition = ref({ x: 0, y: 0 });

function onContextMenu(e: MouseEvent) {
  e.preventDefault();
  menuPosition.value = { x: e.clientX, y: e.clientY };
  isVisible.value = true;
}

function closeMenu() {
  isVisible.value = false;
}
</script>

<template>
  <div @contextmenu="onContextMenu" @click="closeMenu">
    Нажмите правой кнопкой мыши

    <Teleport to="body">
      <div
        v-if="isVisible"
        class="context-menu"
        :style="{ top: menuPosition.y + 'px', left: menuPosition.x + 'px' }"
      >
        <ul>
          <li @click="closeMenu">Копировать</li>
          <li @click="closeMenu">Вставить</li>
          <li @click="closeMenu">Удалить</li>
        </ul>
      </div>
    </Teleport>
  </div>
</template>

7. Подготовка HTML

Не забудьте добавить целевые контейнеры в index.html:

html
<!DOCTYPE html>
<html>
<body>
  <div id="app"></div>
  <div id="modals"></div>
  <div id="notifications"></div>
  <div id="toast-container"></div>
</body>
</html>