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

CaseWinnerWhy
A modal with a form and its own stateTeleport plus a componentThe content stays a Vue component
A short notice without focusTeleport to bodyOne node is enough, no focus management
Focus trap, scroll lock, ESCA headless libraryAccessibility already written and tested
A project with no dependenciesTeleportNothing to install, you write the rest

Common mistakes

  • Putting v-if on 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.