In Vue 3, how does `<Teleport>`'s `disabled` prop render content inline on mobile but in body on desktop, and does state survive toggling?
answer
- a boolean you can bind
- renders between the placeholders
- moved, not re-created
- no missing-target warning
basics
~10 sBinding :disabled="isMobile" on <Teleport> renders the content in place while true and in the target while false. Toggling moves the existing DOM nodes, so component state, input values and lifecycle are preserved; nothing remounts.
solid answer
~40 s`disabled` is a reactive boolean prop on `<Teleport>`. When it is `true`, Vue renders the slot content in its original position, between the Teleport's placeholder nodes; when it is `false`, the content goes into the `to` target. Flipping it at runtime makes Vue **move** the already-mounted nodes between the two places rather than unmounting and remounting, so component instances, refs and typed-in form values survive and `onMounted` does not run again. A disabled Teleport also does not require its target to exist and logs no missing-target warning. Pair it with a `matchMedia` listener to switch a sheet between inline on mobile and an overlay on desktop.
code
vue · 11 lines<script setup lang="ts">
defineProps<{ inline?: boolean }>()
</script>
<template>
<Teleport to="#overlays" :disabled="inline">
<div class="sheet" :class="{ 'sheet--inline': inline }">
<slot />
</div>
</Teleport>
</template>go deeper
Know that disabled makes the Teleport render its content in place instead of in the target.
Explain that toggling moves existing nodes, so instances and form state persist and mount hooks do not re-run, unlike swapping two copies with v-if.
Call out what still changes on a toggle: DOM-ancestry-dependent behaviour, cached measurements, and elements the browser resets when moved. Design styles for both placements.
Use disabled as the standard way to make overlay primitives render inline for tests, stories and narrow layouts, keeping one code path across placements.
## The use case Responsive designs often want the same UI in two placements. A filter panel might be an **overlay** on desktop, rendered at `body` level to escape the page layout, but a plain **inline block** on mobile where it simply stacks in the flow. Writing two components, or two copies of the markup behind `v-if`, duplicates logic and loses state when the layout switches. `<Teleport>`'s `disabled` prop solves this with one element. ## How `disabled` works - `disabled` is a boolean prop and may be bound reactively: `<Teleport to="body" :disabled="isMobile">`. - While it is `true`, the content renders **in place**, between the two placeholder nodes Vue leaves where the Teleport sits in the template. - While it is `false`, the content renders into the target named by `to`. - When the value flips, Vue **moves the existing DOM nodes** between the two locations. It does not tear down the component instances inside. - If `to` changes while the Teleport is enabled, Vue likewise moves the content to the new target. ## What survives a toggle | Kind of state | Survives toggling `disabled`? | Why | |---|---|---| | Refs and reactive state in child components | Yes | The instances are never unmounted | | Values typed into inputs | Yes | The same input elements are moved | | `onMounted` / `onUnmounted` | Not re-run | No mount or unmount happens | | Anything tied to DOM ancestry | Changes | Bubbling, inherited CSS and ancestor selectors now follow the new position | | Some browser-level element state | May reset | The browser performs a remove-and-insert, and certain elements (a moved iframe, for example) can reload | The first three rows are what make `disabled` preferable to rendering two separate copies with `v-if`: the `v-if` approach destroys one subtree and builds another, losing everything the user entered. ## Target resolution when disabled A disabled Teleport does not need its target. Vue still resolves `to`, but it skips mounting into it, and the development warnings about a missing or invalid target are suppressed while `disabled` is true. That makes `disabled` useful in two more places: 1. rendering components that normally teleport inside tests or story files, where no overlay container exists; 2. letting a reusable component offer an `inline` option that simply sets `disabled`. ## A responsive example ```vue <script setup lang="ts"> import { ref, onMounted, onUnmounted } from 'vue' const isMobile = ref(false) let mq: MediaQueryList | undefined const update = () => { isMobile.value = !!mq?.matches } onMounted(() => { mq = window.matchMedia('(max-width: 640px)') update() mq.addEventListener('change', update) }) onUnmounted(() => mq?.removeEventListener('change', update)) </script> <template> <Teleport to="#overlays" :disabled="isMobile"> <FilterPanel /> </Teleport> </template> ``` Resizing across 640px moves `<FilterPanel>` between inline and `#overlays`, and any half-completed filter selection stays put. ## Pitfalls - Styles must work in **both** positions: an overlay layout (fixed, backdrop) and an inline layout. A common approach is a modifier class bound to the same flag. - Because DOM ancestry changes on each toggle, code that measured the element, attached observers to its old parent or cached a `getBoundingClientRect()` should re-measure after the switch. - `disabled` controls placement only. Showing and hiding the content is still the job of `v-if` or `v-show` inside the Teleport. ## `disabled` compared with other placement tools | Tool | What it changes | Component state on switch | |---|---|---| | `:disabled` on one `<Teleport>` | Where the same nodes live | Kept: nodes are moved | | Changing `to` on an enabled `<Teleport>` | Which target the nodes live in | Kept: nodes are moved | | `v-if` / `v-else` between two copies | Which subtree exists at all | Lost: one subtree unmounts, the other mounts | | `v-show` inside the Teleport | Whether the nodes are visible | Kept, but placement never changes | A useful way to phrase it in an interview: `disabled` and `to` are **placement** controls, `v-if` and `v-show` are **presence** and **visibility** controls, and only the placement controls let one mounted subtree change its DOM home without being rebuilt. Combining them is normal: `v-if` decides whether the panel exists, `disabled` decides where it is shown. ## Summary `disabled` turns Teleport into a placement switch: one subtree, two possible DOM homes, moved rather than rebuilt. That preserves component and form state across layout changes and keeps the component usable where no target exists.
- Why prefer toggling `disabled` over rendering two copies of the panel with `v-if` and `v-else`?`v-if`/`v-else` unmounts one subtree and mounts another, so component state, typed input and scroll position are lost whenever the breakpoint flips, and mount hooks run again. Toggling `disabled` keeps one set of instances and just moves their DOM nodes, so nothing is rebuilt.
- What happens if `to` points at a missing element while `disabled` is true?Nothing goes wrong: the content renders in place and Vue does not log its missing-target warnings, because a disabled Teleport never mounts into the target. That is why a component that teleports can be rendered in isolation, such as in a test, by disabling it.
saying these in an interview costs you the question
- Toggling disabled unmounts and remounts the teleported components
- A disabled Teleport renders nothing at all
- The disabled prop is static and cannot be bound to reactive state
- A disabled Teleport still throws when its target is missing
- disabled hides the content, like v-show