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.

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)

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.