{"record":{"id":"5d3c532d1787fa16","repo":"tailwindlabs/headlessui","slug":"did-you-forget-to-passthrough-the-ref-to-the-act","errorCode":null,"errorMessage":"Did you forget to passthrough the `ref` to the actual DOM node?","messagePattern":"Did you forget to passthrough the `ref` to the actual DOM node\\?","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/@headlessui-vue/src/components/transitions/transition.ts","lineNumber":263,"sourceCode":"    let enterFromClasses = splitClasses(props.enterFrom)\n    let enterToClasses = splitClasses(props.enterTo)\n\n    let enteredClasses = splitClasses(props.entered)\n\n    let leaveClasses = splitClasses(props.leave)\n    let leaveFromClasses = splitClasses(props.leaveFrom)\n    let leaveToClasses = splitClasses(props.leaveTo)\n\n    onMounted(() => {\n      watchEffect(() => {\n        if (state.value === TreeStates.Visible) {\n          let domElement = dom(container)\n          // When you return `null` from a component, the actual DOM reference will\n          // be an empty comment... This means that we can never check for the DOM\n          // node to be `null`. So instead we check for an empty comment.\n          let isEmptyDOMNode = domElement instanceof Comment && domElement.data === ''\n          if (isEmptyDOMNode) {\n            throw new Error('Did you forget to passthrough the `ref` to the actual DOM node?')\n          }\n        }\n      })\n    })\n\n    function executeTransition(onInvalidate: (cb: () => void) => void) {\n      // Skipping initial transition\n      let skip = initial.value && !appear.value\n\n      let node = dom(container)\n      if (!node || !(node instanceof HTMLElement)) return\n      if (skip) return\n\n      isTransitioning.value = true\n\n      if (show.value) beforeEnter()\n      if (!show.value) beforeLeave()\n","sourceCodeStart":245,"sourceCodeEnd":281,"githubUrl":"https://github.com/tailwindlabs/headlessui/blob/eea57cf46fd6767ed1059012f7073b88eb159fba/packages/@headlessui-vue/src/components/transitions/transition.ts#L245-L281","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["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).","Give the inner component a single root element so Vue attaches the forwarded ref/attrs automatically.","If you don't need a wrapper, remove it and put the transition classes/props directly on a real element.","Avoid `as=\"template\"` when the children render to nothing or to multiple nodes."],"exampleFix":"<!-- before -->\n<TransitionChild as=\"template\">\n  <MyPanel /> <!-- MyPanel never forwards the ref -->\n</TransitionChild>\n\n<!-- after -->\n<!-- MyPanel.vue -->\n<template><div v-bind=\"attrs\"><slot /></div></template>\n<script setup>\nimport { useAttrs } from 'vue'\nlet attrs = useAttrs()\n</script>","handlingStrategy":"validation","validationCode":"// Before rendering, ensure the child of TransitionChild has a single real element root\n// e.g. check your wrapper component's template has exactly one element and forwards attrs","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Give custom wrapper components inside transitions a single root element that binds v-bind=\"attrs\".","Avoid components returning null inside <TransitionChild>.","Test transitions in dev mode where this assertion throws early."],"tags":["headlessui","vue","transition","ref","dom"],"backgroundTag":"ref-not-forwarded-to-dom","analyzedSha":"eea57cf46fd6767ed1059012f7073b88eb159fba","analyzedAt":"2026-08-28T19:22:46.163Z","schemaVersion":2},"datasetVersion":"2026-08-28T21:17:43.275Z"}