tailwindlabs/headlessui · error · Error

Did you forget to passthrough the `ref` to the actual DOM no

Error message

Did you forget to passthrough the `ref` to the actual DOM node?

What it means

Headless UI Vue's <TransitionChild>/<Transition> requires the transition to be attached to a real DOM node. After resolving the container via `dom(container)`, the library checks whether the resolved node is an empty Vue comment placeholder (`Comment` node with empty data), which is what Vue renders when a component returns null/renders nothing. If so, the `ref` you were given was never forwarded to an actual element, so the transition has nothing to animate.

Source

Thrown at packages/@headlessui-vue/src/components/transitions/transition.ts:263

    let enterFromClasses = splitClasses(props.enterFrom)
    let enterToClasses = splitClasses(props.enterTo)

    let enteredClasses = splitClasses(props.entered)

    let leaveClasses = splitClasses(props.leave)
    let leaveFromClasses = splitClasses(props.leaveFrom)
    let leaveToClasses = splitClasses(props.leaveTo)

    onMounted(() => {
      watchEffect(() => {
        if (state.value === TreeStates.Visible) {
          let domElement = dom(container)
          // When you return `null` from a component, the actual DOM reference will
          // be an empty comment... This means that we can never check for the DOM
          // node to be `null`. So instead we check for an empty comment.
          let isEmptyDOMNode = domElement instanceof Comment && domElement.data === ''
          if (isEmptyDOMNode) {
            throw new Error('Did you forget to passthrough the `ref` to the actual DOM node?')
          }
        }
      })
    })

    function executeTransition(onInvalidate: (cb: () => void) => void) {
      // Skipping initial transition
      let skip = initial.value && !appear.value

      let node = dom(container)
      if (!node || !(node instanceof HTMLElement)) return
      if (skip) return

      isTransitioning.value = true

      if (show.value) beforeEnter()
      if (!show.value) beforeLeave()

View on GitHub (pinned to eea57cf46f)

Solutions

  1. Ensure the component inside <TransitionChild> forwards the ref to its root element: accept `props` and spread them via `v-bind="attrs"` (see the `forwardRefs`/`useRender` helpers in headlessui-vue).
  2. Give the inner component a single root element so Vue attaches the forwarded ref/attrs automatically.
  3. If you don't need a wrapper, remove it and put the transition classes/props directly on a real element.
  4. Avoid `as="template"` when the children render to nothing or to multiple nodes.

Example fix

<!-- before -->
<TransitionChild as="template">
  <MyPanel /> <!-- MyPanel never forwards the ref -->
</TransitionChild>

<!-- after -->
<!-- MyPanel.vue -->
<template><div v-bind="attrs"><slot /></div></template>
<script setup>
import { useAttrs } from 'vue'
let attrs = useAttrs()
</script>
Defensive patterns

Strategy: validation

Validate before calling

// Before rendering, ensure the child of TransitionChild has a single real element root
// e.g. check your wrapper component's template has exactly one element and forwards attrs

Prevention

When it happens

Trigger: Rendering <TransitionChild> whose child component swallows the `ref` (e.g., a custom wrapper with `inheritAttrs: false` or multiple root nodes, or returning null), or using `as="template"` around a component that doesn't use `useRenderStrategyFeatures`/forwards attrs, so `dom()` resolves to an empty comment node instead of an element.

Common situations: Wrapping Dialog/Popover/Menu panels in custom components; forgetting `v-bind="$attrs"` and `v-bind="attrs"` passthrough; single-file components with multiple root nodes (fragments) so Vue can't auto-attach the ref; conditional rendering that yields null during transition.

Related errors


AI-assisted analysis of tailwindlabs/headlessui@eea57cf46f (2026-08-28). Data as JSON: /api/errors/5d3c532d1787fa16. Report an issue: GitHub.