Vue.js 3 Component Teleportation Tutorial

In this Vue tutorial we learn how to teleport components to other places in the DOM.

We how to use the teleport component and some situations where you might want to use it.

Lesson Video

If you prefer to learn visually, you can watch this lesson in video format.

Lesson Project

If you want to follow along with the examples, you will need an app generated by the Vue CLI as well as the following extra components.

  • src/components/StepA.vue
  • src/components/StepB.vue

The project should look similar to the following.

Example: project
project-folder/
├── src/
|   ├── components/
|   |   ├── StepA.vue
|   |   └── StepB.vue
|   └── App.vue

We want the new components to be nested inside the root app component.

Example: src/App.vue
<template>
  <step-a />
  <step-B />
</template>

<script>
import StepA from "./components/StepA";
import StepB from "./components/StepB";

export default {
  components: {
    StepA,
    StepB
  }
}
</script>

The two new components can each have a heading with some identifying text.

Example: src/components/StepA.vue
<template>
  <h2>Step A Component</h2>
</template>
Example: src/components/StepB.vue
<template>
  <h2>Step B Component</h2>
</template>

How to teleport components to a different place in the DOM tree

The Vue team introduced a new feature in the Vue 3 core library called component teleporting.

Teleporting allows us to define a component in one place, then render it somewhere else in the DOM tree, even outside the scope of the Vue app.

By default, we mount our application onto a single div element in the index.html file. If we open the /public/index.html file, we should see the div just above the closing body tag.

Example: public/index.html
...
  <div id="app"></div>
  <!-- built files will be auto injected -->
</body>
...

Our application is mounted to that node in the DOM and all the components we define will be injected into it. If we try to use the components outside of the div, they won’t work.

The teleport built-in component gives us the ability to break out of that DOM tree. So we can render a component into a DOM node that’s not in <div id="app"> .

To use the teleport component, we wrap the component we wish to teleport with open-and-close teleport tags. Then we use the to prop to tell Vue where we want to mount it with any valid Javascript querySelector .

Syntax: teleport
<teleport to="querySelector">
  <component_name />
</teleport>

To demonstrate, let’s modify our multi-step form example to render one of the step components in the default DOM tree, and another outside of it.

We will start in /public/index.html and create a new div below the app div. We will give it an id of port and add some inline styling to help with the demonstration. This is where we will mount one of the step components.

Example: public/index.html
<!DOCTYPE html>
<html lang="">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
    <title><%= htmlWebpackPlugin.options.title %></title>
  </head>
  <body>
    <noscript>
      <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
    </noscript>

    <div id="app"></div>
    <div id="port" style="padding:1rem;background:#232323;color:#D1D1D1"></div>

  </body>
</html>

In the root App component, we’ll teleport the StepB component to the #port div.

Example: src/App.vue
<template>
  <step-a />

  <teleport to="#port">
    <step-b />
  </teleport>
</template>

<script>
import StepA from "./components/StepA.vue";
import StepB from "./components/StepB.vue";

export default {
  components: { StepA, StepB }
}
</script>

When we save the files and take a look in the browser, everything in the StepB component will have the styling of the #port div, confirming that it has been teleported.

Teleport use case

Ideally, we don’t want to perform DOM manipulations ourselves. Instead, we want Vue to manipulate the DOM for us. But there will be some situations where it makes sense to teleport a component, like a modal.

To demonstrate, let’s create a new component called MainModal.vue .

Example: project
project-folder/
├── src/
|   ├── components/
|   |   ├── StepA.vue
|   |   ├── StepB.vue
|   |   └── MainModal.vue
|   └── App.vue

We’ll make a simple box with a heading and some dummy content that emits an event to close the modal when the user clicks on an overlay.

Example: src/components/MainModal.vue
<template>
  <div class="modal-overlay" @click="$emit('close')">
    <div class="modal">
      <h2>Modal</h2>
      <p>
        Lorem ipsum dolor sit amet consectetur adipisicing
        elit. Accusamus perferendis unde saepe rem dolorem
        deleniti minima dolor. Veritatis facilis, rerum
        recusandae corrupti ea eveniet error atque, animi
        labore earum eum!
      </p>
    </div>
  </div>
</template>

<script>
export default {
  emits: ['close']
}
</script>

<style scoped>
.modal-overlay {
  display:grid;place-items:center;
  position:absolute;top:0;bottom:0;
  left:0;right:0;background:rgba(0,0,0,.4)
}
.modal {
  position:relative;width:260px;padding:20px;
  margin:20px;border-radius:6px;background:#F9FAFB;
  box-shadow:0 10px 15px -3px rgba(0,0,0,.3),
             0 4px 6px -2px rgba(0,0,0,.05),
             0 2px 4px -1px rgba(0,0,0,.06)
}
</style>

In /public/index.html , we change the #port div to #modal and remove the styling. This is where we’ll mount the modal.

Example: public/index.html
<!DOCTYPE html>
<html lang="">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
    <title><%= htmlWebpackPlugin.options.title %></title>
  </head>
  <body>
    <noscript>
      <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
    </noscript>

    <div id="app"></div>
    <div id="modal"></div>

  </body>
</html>

Finally, the root App component will have some content and a button that opens the modal. For the close functionality, we attach the close emitted event to the component as a click event.

Example: src/App.vue
<template>
  <div class="container">
    <div class="content">
      <h2>Modal demonstration</h2>
      <p>
        Lorem ipsum dolor sit amet consectetur adipisicing
        elit. Accusamus perferendis unde saepe rem dolorem
        deleniti minima dolor. Veritatis facilis, rerum
        recusandae corrupti ea eveniet error atque, animi
        labore earum eum!
      </p>
      <button @click="showModal = true">Show Modal</button>

      <teleport to="#modal">
        <main-modal v-show="showModal" @close="showModal = false" />
      </teleport>
    </div>
  </div>
</template>

<script>
import MainModal from "./components/MainModal.vue";

export default {
  components: { MainModal },
  data() {
    return { showModal: false }
  }
}
</script>

<style scoped>
.container {
  display:grid;place-items:center;
  position:absolute;
  top:0;bottom:0;left:0;right:0;
}
.content {
  position:relative;width:400px;
  padding:20px;font-size:18px;
}
</style>

If we click on the “Show Modal” button in the browser, the overlay covers the page and the modal appears. Clicking anywhere will close it again.

This behavior is exactly what we want, and why the Vue team added teleportation.

Let’s see what happens if we don’t teleport the modal outside the root App DOM tree. Remove or comment out the teleport tags from around the main-modal component.

Example: src/App.vue
<template>
  <div class="container">
    <div class="content">
      <h2>Modal demonstration</h2>
      <p>
        Lorem ipsum dolor sit amet consectetur adipisicing
        elit. Accusamus perferendis unde saepe rem dolorem
        deleniti minima dolor. Veritatis facilis, rerum
        recusandae corrupti ea eveniet error atque, animi
        labore earum eum!
      </p>
      <button @click="showModal = true">Show Modal</button>

      <!-- <teleport to="#modal"> -->
        <main-modal v-show="showModal" @close="showModal = false" />
      <!-- </teleport> -->
    </div>
  </div>
</template>

<script>
import MainModal from "./components/MainModal.vue";

export default {
  components: { MainModal },
  data() {
    return { showModal: false }
  }
}
</script>

<style scoped>
.container {
  display:grid;place-items:center;
  position:absolute;
  top:0;bottom:0;left:0;right:0;
}
.content {
  position:relative;width:400px;
  padding:20px;font-size:18px;
}
</style>

When we open the modal now, the overlay shows in the content div instead of across the whole page. That’s because it’s back in the same DOM tree, so in this case it’s better to teleport.

Something to note however is that we set up the example to break on purpose when it’s not teleported.

If we take a closer look at the CSS, we can see that the content div’s position is relative to its parent, the container div. If we comment out the position:relative property, the overlay shows across the whole page again.

Example: src/App.vue
<style scoped>
.container {
  display:grid;place-items:center;
  position:absolute;
  top:0;bottom:0;left:0;right:0;
}
.content {
  /*position:relative;*/width:400px;
  padding:20px;font-size:18px;
}
</style>

This is a much simpler solution if you can adjust the styling of your app. It may be that you need the position set to relative, in which case you will need to teleport the component.