Introduction

Vue 3's <Teleport> component renders its content at a different location in the DOM than its logical position in the component tree. When the target element does not exist at render time, Vue logs a warning and the teleported content is not rendered:

bash
[Vue warn]: Failed to locate Teleport target with selector "#modal-container"

This commonly occurs with modals, tooltips, and dropdowns that need to escape overflow:hidden containers.

Symptoms

  • Teleported content does not appear on the page
  • Console warning: "Failed to locate Teleport target"
  • Modal/dialog components render inside the parent instead of the target container
  • Works in development but fails in production builds
  • Works on client-side navigation but fails on initial page load

Common Causes

  • Target element does not exist in the DOM when the Teleport component renders
  • Selector syntax is incorrect (missing # for IDs, . for classes)
  • SSR renders the Teleport on the server but the target element is not in the server HTML
  • Target element is inside another Vue component that has not yet mounted
  • Dynamic target selector changes after the Teleport has already rendered

Step-by-Step Fix

  1. 1.Ensure the target element exists in the HTML before Vue renders:
  2. 2.```html
  3. 3.<!-- In public/index.html, add the target element -->
  4. 4.<body>
  5. 5.<div id="app"></div>
  6. 6.<div id="modal-container"></div>
  7. 7.<div id="toast-container"></div>
  8. 8.</body>
  9. 9.`
  10. 10.Use the correct selector syntax:
  11. 11.```html
  12. 12.<!-- Correct: # for ID, . for class -->
  13. 13.<Teleport to="#modal-container">
  14. 14.<ModalContent />
  15. 15.</Teleport>

<Teleport to=".toast-wrapper"> <ToastList /> </Teleport>

<!-- Also valid: query selector --> <Teleport to="body > .modal-target"> <Dialog /> </Teleport> ```

  1. 1.Handle SSR by disabling Teleport on the server:
  2. 2.```html
  3. 3.<Teleport to="#modal-container" :disabled="isSSR">
  4. 4.<ModalContent />
  5. 5.</Teleport>
  6. 6.`
  7. 7.```javascript
  8. 8.// In setup
  9. 9.import { ref, onMounted } from 'vue';
  10. 10.const isSSR = ref(true);
  11. 11.onMounted(() => { isSSR.value = false; });
  12. 12.`
  13. 13.Use v-if to defer Teleport until after the component is mounted:
  14. 14.```html
  15. 15.<template>
  16. 16.<div v-if="isMounted">
  17. 17.<Teleport to="#modal-container">
  18. 18.<Modal />
  19. 19.</Teleport>
  20. 20.</div>
  21. 21.</template>

<script setup> import { ref, onMounted } from 'vue'; const isMounted = ref(false); onMounted(() => { isMounted.value = true; }); </script> ```

  1. 1.For dynamic targets, use a ref instead of a selector:
  2. 2.```html
  3. 3.<template>
  4. 4.<div ref="targetRef"></div>
  5. 5.<Teleport :to="targetRef">
  6. 6.<Tooltip />
  7. 7.</Teleport>
  8. 8.</template>

<script setup> import { ref } from 'vue'; const targetRef = ref(null); </script> ```

Prevention

  • Always include Teleport target elements in your base HTML template (index.html)
  • Use a layout component that guarantees target elements exist before child components render
  • Add Teleport target verification to your SSR hydration checklist
  • Document all Teleport targets in your component library documentation
  • Test Teleport components on both client-side navigation and full page load
  • Use a global Teleport manager component that creates target elements dynamically if needed
  • In testing, ensure the test DOM includes the target elements before mounting Teleport components