React
Teleport in Vue 3: modals outside the parent DOM
Teleport moves a modal under body, where no parent overflow or transform can clip it. The component stays the same, only the node changes.
Key points
- Teleport moves the DOM, not the instance: provide, inject and emit keep working.
- Put v-if inside the Teleport, or the node is created and destroyed on every open.
- Teleport does not handle focus trap, scroll lock or closing on ESC: you still write those.
A modal in Vue 3 leaves the parent visual tree with <Teleport>: the node renders under body, where no overflow: hidden or transform can clip it.
The problem it solves
A panel with position: fixed positions itself against the nearest ancestor with transform, filter or perspective, not against the viewport. Inside an animated card the modal shows up clipped and the overlay covers half the screen.
import { ref } from "vue";
const open = ref(false);
Open
Title
Close
The to attribute takes a CSS selector or an element. The v-if lives inside the Teleport, so nothing exists in the DOM while the modal is closed.
A dedicated container, or none
...
-->
An empty container in index.html avoids leaning on body: if another script mounts something on the body, your node stays yours. With :disabled the Teleport switches off and the content returns in place, which helps in tests.
Teleport, native dialog, library
| Case | Winner | Why |
|---|---|---|
| A modal with a form and its own state | Teleport plus a component | The content stays a Vue component |
| A short notice without focus | Teleport to body | One node is enough, no focus management |
| Focus trap, scroll lock, ESC | A headless library | Accessibility already written and tested |
| A project with no dependencies | Teleport | Nothing to install, you write the rest |
Common mistakes
- Putting
v-ifon the Teleport instead of the content: the target node is born and dies on every open. - Pointing at an id that does not exist: Vue warns in the console and renders nothing.
- Forgetting the focus: the modal opens with the caret still on the button behind it.
- Leaving the Teleport on in a test: the content lands outside the mounted wrapper and queries fail.
The limitation to plan for
Teleport moves the DOM, not the context: provide, inject, emit and listeners keep working because the instance does not change. It does not solve the focus trap or the scroll lock, which you still write or take from a headless library.
Where to go next
In the catalogue: shadcn-vue, PrimeVue and vue-bits; dialog blocks live in /blocks. The guides: Composition API and Pinia. The comparisons: portals in React and Vue vs React.
Frequently asked questions
Can I use Teleport for an accessible modal?
Yes, but Teleport only moves the DOM node. Focus trap, scroll lock and closing on ESC are yours to write or to take from a headless library.
Does the component lose its context?
No. The instance stays the same, so provide, inject, emit and reactive state keep working after the move.
Why does the modal not appear?
The to selector matches nothing. Vue logs a warning and skips rendering, so check that the target node exists before mount.
Does Teleport work with server-side rendering?
Yes, with one caveat: on the server the content renders where it is declared. Use the disabled attribute during hydration to avoid a mismatch.
Do I need a dedicated container?
Not strictly. body works, but your own node avoids clashes with other scripts that mount on the same body.