Two built-in components solve problems that ordinary components cannot. <Teleport> renders part of a template somewhere else in the DOM, which is essential for modals, toasts and dropdowns that must escape a parent's overflow or z-index. <Suspense> waits for asynchronous child components and shows a fallback until they are ready. After this lesson you will build a correctly layered modal and coordinate loading states across a subtree.
A modal defined deep inside a card component is still a DOM descendant of that card. If the card has overflow: hidden, transform, or a low z-index, the modal is clipped or covered no matter what CSS you write. The logical owner of the modal (the card) and its physical position (top of the page) need to differ.
<Teleport to="selector"> moves its children to the target element while keeping them a logical part of the component: props, events, provide/inject and reactivity all continue to work.
<!-- ConfirmDialog.vue -->
<script setup>
defineProps({ open: Boolean })
const emit = defineEmits(['close'])
</script>
<template>
<Teleport to="body">
<div v-if="open" class="backdrop" @click.self="emit('close')">
<div class="dialog" role="dialog" aria-modal="true">
<slot />
<button @click="emit('close')">Close</button>
</div>
</div>
</Teleport>
</template>Key details:
to accepts a CSS selector or an actual DOM element. The target must exist when the component mounts; a dedicated <div id="modals"> in index.html is a common choice.:disabled="true" renders the content in place instead, which is useful for responsive layouts (a sidebar that is inline on desktop but a drawer on mobile).defer, which lets the target be rendered later in the same render cycle by another part of the app.<Teleport> only moves DOM nodes; it does not manage focus or keyboard handling. Add focus management and Escape handling yourself for accessible dialogs.
A component can have an async setup: <script setup> with a top-level await. Such a component cannot render until the promise resolves. <Suspense> coordinates this by rendering a fallback until every async descendant in its default slot has resolved:
<!-- UserProfile.vue -->
<script setup>
const props = defineProps({ id: Number })
const res = await fetch(`/api/users/${props.id}`)
const user = await res.json()
</script>
<template>
<h2>{{ user.name }}</h2>
</template><!-- Parent -->
<template>
<Suspense>
<UserProfile :id="42" />
<template #fallback>
<p>Loading profile...</p>
</template>
</Suspense>
</template><Suspense> waits for two kinds of dependencies: components with async setup and components created with defineAsyncComponent (unless the async component is given suspensible: false). Once every dependency resolves, the default slot appears in one go, so nested loading spinners do not flicker independently.
Events tell you what is happening: @pending when a pending state begins, @resolve when the default slot resolves, @fallback when the fallback is shown. A timeout prop delays showing the fallback so quick loads do not flash it.
<Suspense> has no error slot. Errors thrown by async setup propagate to the nearest onErrorCaptured hook, so the parent can catch them and render an error state:
<script setup>
import { ref, onErrorCaptured } from 'vue'
const error = ref(null)
onErrorCaptured((err) => {
error.value = err
return false // stop propagation
})
</script>
<template>
<div v-if="error">Could not load: {{ error.message }}</div>
<Suspense v-else>
<UserProfile :id="42" />
<template #fallback>Loading...</template>
</Suspense>
</template><Suspense> is still marked experimental in the official documentation; its API is stable in practice, but consider a loading ref in a composable for simple cases.<Suspense> with <RouterView v-slot="{ Component }"> and <component :is="Component"> to show one fallback for lazily loaded, data-fetching pages..backdrop, .toast) are easy to target.Why does a modal rendered with `<Teleport to="body">` still receive props and events from its parent component?
<Teleport to> renders children at another DOM location while keeping them logically inside the component.disabled and defer cover responsive and ordering edge cases.<script setup> with top-level await makes a component async; <Suspense> shows #fallback until it resolves.pending, resolve and fallback.onErrorCaptured in an ancestor.Next lesson: Transition and TransitionGroup — animate elements entering, leaving and moving with CSS classes and JavaScript hooks.