tailwindlabs/headlessui · error · Error

[Headless UI]: Cannot find ownerDocument for contextElement

Error message

[Headless UI]: Cannot find ownerDocument for contextElement: ${contextElement}

What it means

The Vue Portal resolves its mount target by walking from a context element to its ownerDocument. If getOwnerDocument() returns nothing for a non-null contextElement, the element is detached from any document (or is not a real DOM node), so the portal has nowhere to attach and throws this diagnostic error.

Solutions

  1. Ensure the contextElement is attached to the document before the portal resolves (defer with onMounted/nextTick in SSR setups).
  2. Pass a real, connected DOM element (or null to use the default) rather than a detached or manually created node.
  3. In SSR, skip portal rendering until client hydration (e.g. wrap in <ClientOnly> in Nuxt).

Example fix

// before
<Portal :target="detachedEl">...</Portal> <!-- detachedEl not in document -->

// after
<Portal>...</Portal> <!-- let Headless UI resolve the default portal root -->
Defensive patterns

Strategy: type-guard

Validate before calling

function isAttachedToDocument(el: Element | null): boolean {
  return el !== null && el.isConnected && el.ownerDocument != null
}

// only pass the element when it is connected
<Portal :context-element="isAttachedToDocument(el) ? el : undefined">

Type guard

const isConnectedElement = (el: unknown): el is Element & { ownerDocument: Document } => el instanceof Element && el.isConnected && el.ownerDocument != null

Try / catch

try { getPortalRoot() } catch (e) { if (e instanceof Error && e.message.includes('Cannot find ownerDocument')) { /* fall back to document.body via nextTick */ } else throw e }

Prevention

When it happens

Trigger: Rendering the Portal during SSR or before the element is inserted into the document; passing a contextElement that was created via createElement but never appended; passing a mock/fake element (e.g. jsdom stub or test double) that lacks proper node ownership.

Common situations: Server-side rendering with Nuxt where the portal mounts before hydration; unit tests with fake DOM nodes; teleporting from an element that has been removed from the DOM in the same tick.

Related errors


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

Appendix: source

Thrown at packages/@headlessui-vue/src/components/portal/portal.ts:35

  type Ref,
} from 'vue'
import { usePortalRoot } from '../../internal/portal-force-root'
import { dom } from '../../utils/dom'
import { getOwnerDocument } from '../../utils/owner'
import { render } from '../../utils/render'

type ContextType<T> = T extends InjectionKey<infer V> ? V : never

// ---

function getPortalRoot(contextElement?: HTMLElement | null) {
  let ownerDocument = getOwnerDocument(contextElement)
  if (!ownerDocument) {
    if (contextElement === null) {
      return null
    }

    throw new Error(
      `[Headless UI]: Cannot find ownerDocument for contextElement: ${contextElement}`
    )
  }
  let existingRoot = ownerDocument.getElementById('headlessui-portal-root')
  if (existingRoot) return existingRoot

  let root = ownerDocument.createElement('div')
  root.setAttribute('id', 'headlessui-portal-root')
  return ownerDocument.body.appendChild(root)
}

export let Portal = defineComponent({
  name: 'Portal',
  props: {
    as: { type: [Object, String], default: 'div' },
  },
  setup(props, { slots, attrs }) {
    let element = ref<HTMLElement | null>(null)

View on GitHub (pinned to eea57cf46f)