skip to content

In Vue 3, how does `<Teleport>`'s `disabled` prop render content inline on mobile but in body on desktop, and does state survive toggling?

level: middleimportance: should knowfreq 40%

answer

  1. a boolean you can bind
  2. renders between the placeholders
  3. moved, not re-created
  4. no missing-target warning

basics

~10 s

Binding :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
vue
<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

for a junior

Know that disabled makes the Teleport render its content in place instead of in the target.

for a middle

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.

for a senior

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.

for a principal

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