Repository: tailwindlabs/headlessui
Pull request #3704 (2025-04-24 17:02:36)

Fix focus not returned to SVG Element
This PR fixes an issue where the focus is not returned to an `SVG` element with a `tabIndex` correctly.

There are a few issues going on here:

1. We assume that the element to focus (`e.target`) is an instanceof `HTMLElement`, but the `SVGElement` is not an instanceof `HTMLElement`.
2. By using `instanceof` we are checking against concrete classes, so if this happen to cross certain contexts (Shadow DOM, Iframes, ...) then the instances would be different.

To solve this, we will now:

1. Relax the types and only care about the actual attributes and methods we are interested in. In most cases this means changing internal types from `HTMLElement` to `Element` for example.
2. We will check whether certain properties are available in the object to deduce the correct type from the object.

Fixes: #3660

## Test plan

Added an SVG to open a Dialog component and made sure that pressing `escape` or clicking outside of the Dialog does restore the focus to the SVG itself.
```tsx
<svg
  tabIndex={0}
  onKeyDown={(e) => {
    if (e.key === 'Enter' || e.key === ' ') {
      setIsOpen((v) => !v)
    }
  }}
  onClick={() => setIsOpen((v) => !v)}
  className="h-6 w-6 text-gray-500"
>
  <BookOpenIcon />
</svg>
```


Here is a video of that behavior:

https://github.com/user-attachments/assets/1805ca67-8bc7-4315-98a7-2490cba9230c



vercel[bot]: [vc]: #B/bUG1x7DcGr4u50xaAJGcKiDXqQpjWIg+KaciH8asc=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJoZWFkbGVzc3VpLXJlYWN0Iiwicm9vdERpcmVjdG9yeSI6InBsYXlncm91bmRzL3JlYWN0IiwiaW5zcGVjdG9yVXJsIjoiaHR0cHM6Ly92ZXJjZWwuY29tL3RhaWx3aW5kbGFicy9oZWFkbGVzc3VpLXJlYWN0LzREOEo0c2J5a3NqYXo5NEFaSjRtRll0dXJ5d0wiLCJwcmV2aWV3VXJsIjoiaGVhZGxlc3N1aS1yZWFjdC1naXQtZml4LWlzc3VlLTM2NjAtdGFpbHdpbmRsYWJzLnZlcmNlbC5hcHAiLCJuZXh0Q29tbWl0U3RhdHVzIjoiUEVORElORyIsImxpdmVGZWVkYmFjayI6eyJyZXNvbHZlZCI6MCwidW5yZXNvbHZlZCI6MCwidG90YWwiOjAsImxpbmsiOiJoZWFkbGVzc3VpLXJlYWN0LWdpdC1maXgtaXNzdWUtMzY2MC10YWlsd2luZGxhYnMudmVyY2VsLmFwcCJ9fSx7Im5hbWUiOiJoZWFkbGVzc3VpLXZ1ZSIsInJvb3REaXJlY3RvcnkiOiJwbGF5Z3JvdW5kcy92dWUiLCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vdGFpbHdpbmRsYWJzL2hlYWRsZXNzdWktdnVlL0RaOUV4bjVjWjhEY0dGMlJKUGlmTlRGUlB1UnciLCJwcmV2aWV3VXJsIjoiaGVhZGxlc3N1aS12dWUtZ2l0LWZpeC1pc3N1ZS0zNjYwLXRhaWx3aW5kbGFicy52ZXJjZWwuYXBwIiwibmV4dENvbW1pdFN0YXR1cyI6IlBFTkRJTkciLCJsaXZlRmVlZGJhY2siOnsicmVzb2x2ZWQiOjAsInVucmVzb2x2ZWQiOjAsInRvdGFsIjowLCJsaW5rIjoiaGVhZGxlc3N1aS12dWUtZ2l0LWZpeC1pc3N1ZS0zNjYwLXRhaWx3aW5kbGFicy52ZXJjZWwuYXBwIn19XX0=
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **headlessui-react** | 🔄 Building ([Inspect](https://vercel.com/tailwindlabs/headlessui-react/4D8J4sbyksjaz94AZJ4mFYturywL)) | [Visit Preview](https://headlessui-react-git-fix-issue-3660-tailwindlabs.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/headlessui-react-git-fix-issue-3660-tailwindlabs.vercel.app?via=pr-comment-feedback-link) | Apr 24, 2025 5:02pm |
| **headlessui-vue** | 🔄 Building ([Inspect](https://vercel.com/tailwindlabs/headlessui-vue/DZ9Exn5cZ8DcGF2RJPifNTFRPuRw)) | [Visit Preview](https://headlessui-vue-git-fix-issue-3660-tailwindlabs.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/headlessui-vue-git-fix-issue-3660-tailwindlabs.vercel.app?via=pr-comment-feedback-link) | Apr 24, 2025 5:02pm |




Patch:
diff --git a/packages/@headlessui-react/CHANGELOG.md b/packages/@headlessui-react/CHANGELOG.md
index a540376..1d9d0bf 100644
--- a/packages/@headlessui-react/CHANGELOG.md
+++ b/packages/@headlessui-react/CHANGELOG.md
@@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 - Fix clicking `Label` component should open `<Input type="file">` ([#3707](https://github.com/tailwindlabs/headlessui/pull/3707))
 - Ensure clicking on interactive elements inside `Label` component works ([#3709](https://github.com/tailwindlabs/headlessui/pull/3709))
+- Fix focus not returned to SVG Element ([#3704](https://github.com/tailwindlabs/headlessui/pull/3704))
 
 ## [2.2.2] - 2025-04-17
 
diff --git a/packages/@headlessui-react/src/components/combobox/combobox.tsx b/packages/@headlessui-react/src/components/combobox/combobox.tsx
index 3bb115f..70f63ae 100644
--- a/packages/@headlessui-react/src/components/combobox/combobox.tsx
+++ b/packages/@headlessui-react/src/components/combobox/combobox.tsx
@@ -63,6 +63,7 @@ import { history } from '../../utils/active-element-history'
 import { isDisabledReactIssue7711 } from '../../utils/bugs'
 import { Focus } from '../../utils/calculate-active-index'
 import { disposables } from '../../utils/disposables'
+import * as DOM from '../../utils/dom'
 import { match } from '../../utils/match'
 import { isMobile } from '../../utils/platform'
 import {
@@ -1012,8 +1013,8 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
         }
 
         let option = e.target.closest('[role="option"]:not([data-disabled])')
-        if (option !== null) {
-          return QuickReleaseAction.Select(option as HTMLElement)
+        if (DOM.isHTMLElement(option)) {
+          return QuickReleaseAction.Select(option)
         }
 
         if (optionsElement?.contains(e.target)) {
diff --git a/packages/@headlessui-react/src/components/disclosure/disclosure.tsx b/packages/@headlessui-react/src/components/disclosure/disclosure.tsx
index 03a6e92..781c8b5 100644
--- a/packages/@headlessui-react/src/components/disclosure/disclosure.tsx
+++ b/packages/@headlessui-react/src/components/disclosure/disclosure.tsx
@@ -35,6 +35,7 @@ import {
 } from '../../internal/open-closed'
 import type { Props } from '../../types'
 import { isDisabledReactIssue7711 } from '../../utils/bugs'
+import * as DOM from '../../utils/dom'
 import { match } from '../../utils/match'
 import { getOwnerDocument } from '../../utils/owner'
 import {
@@ -185,7 +186,7 @@ function DisclosureFn<TTag extends ElementType = typeof DEFAULT_DISCLOSURE_TAG>(
     ref,
     optionalRef(
       (ref) => {
-        internalDisclosureRef.current = ref as HTMLElement | null
+        internalDisclosureRef.current = ref
       },
       props.as === undefined ||
         // @ts-expect-error The `as` prop _can_ be a Fragment
@@ -202,22 +203,26 @@ function DisclosureFn<TTag extends ElementType = typeof DEFAULT_DISCLOSURE_TAG>(
   } as StateDefinition)
   let [{ disclosureState, buttonId }, dispatch] = reducerBag
 
-  let close = useEvent((focusableElement?: HTMLElement | MutableRefObject<HTMLElement | null>) => {
-    dispatch({ type: ActionTypes.CloseDisclosure })
-    let ownerDocument = getOwnerDocument(internalDisclosureRef)
-    if (!ownerDocument) return
-    if (!buttonId) return
+  let close = useEvent(
+    (focusableElement?: HTMLOrSVGElement | MutableRefObject<HTMLOrSVGElement | null>) => {
+      dispatch({ type: ActionTypes.CloseDisclosure })
+      let ownerDocument = getOwnerDocument(internalDisclosureRef)
+      if (!ownerDocument) return
+      if (!buttonId) return
 
-    let restoreElement = (() => {
-      if (!focusableElement) return ownerDocument.getElementById(buttonId)
-      if (focusableElement instanceof HTMLElement) return focusableElement
-      if (focusableElement.current instanceof HTMLElement) return focusableElement.current
+      let restoreElement = (() => {
+        if (!focusableElement) return ownerDocument.getElementById(buttonId)
+        if (DOM.isHTMLorSVGElement(focusableElement)) return focusableElement
+        if ('current' in focusableElement && DOM.isHTMLorSVGElement(focusableElement.current)) {
+          return focusableElement.current
+        }
 
-      return ownerDocument.getElementById(buttonId)
-    })()
+        return ownerDocument.getElementById(buttonId)
+      })()
 
-    restoreElement?.focus()
-  })
+      restoreElement?.focus()
+    }
+  )
 
   let api = useMemo<ContextType<typeof DisclosureAPIContext>>(() => ({ close }), [close])
 
diff --git a/packages/@headlessui-react/src/components/focus-trap/focus-trap.tsx b/packages/@headlessui-react/src/components/focus-trap/focus-trap.tsx
index 482f2dd..20a084f 100644
--- a/packages/@headlessui-react/src/components/focus-trap/focus-trap.tsx
+++ b/packages/@headlessui-react/src/components/focus-trap/focus-trap.tsx
@@ -21,6 +21,7 @@ import { useWatch } from '../../hooks/use-watch'
 import { Hidden, HiddenFeatures } from '../../internal/hidden'
 import type { Props } from '../../types'
 import { history } from '../../utils/active-element-history'
+import * as DOM from '../../utils/dom'
 import { Focus, FocusResult, focusElement, focusIn } from '../../utils/focus-management'
 import { match } from '../../utils/match'
 import { microTask } from '../../utils/micro-task'
@@ -28,18 +29,18 @@ import { forwardRefWithAs, useRender, type HasDisplayName, type RefProp } from '
 
 type Containers =
   // Lazy resolved containers
-  | (() => Iterable<HTMLElement>)
+  | (() => Iterable<Element>)
 
   // List of containers
-  | MutableRefObject<Set<MutableRefObject<HTMLElement | null>>>
+  | MutableRefObject<Set<MutableRefObject<Element | null>>>
 
-function resolveContainers(containers?: Containers): Set<HTMLElement> {
+function resolveContainers(containers?: Containers): Set<Element> {
   if (!containers) return new Set<HTMLElement>()
   if (typeof containers === 'function') return new Set(containers())
 
-  let all = new Set<HTMLElement>()
+  let all = new Set<Element>()
   for (let container of containers.current) {
-    if (container.current instanceof HTMLElement) {
+    if (DOM.isElement(container.current)) {
       all.add(container.current)
     }
   }
@@ -121,8 +122,8 @@ function FocusTrapFn<TTag extends ElementType = typeof DEFAULT_FOCUS_TRAP_TAG>(
 
   let direction = useTabDirection()
   let handleFocus = useEvent((e: ReactFocusEvent) => {
-    let el = container.current as HTMLElement
-    if (!el) return
+    if (!DOM.isHTMLElement(container.current)) return
+    let el = container.current
 
     // TODO: Cleanup once we are using real browser tests
     let wrapper = process.env.NODE_ENV === 'test' ? microTask : (cb: Function) => cb()
@@ -163,10 +164,10 @@ function FocusTrapFn<TTag extends ElementType = typeof DEFAULT_FOCUS_TRAP_TAG>(
       if (!(features & FocusTrapFeatures.FocusLock)) return
 
       let allContainers = resolveContainers(containers)
-      if (container.current instanceof HTMLElement) allContainers.add(container.current)
+      if (DOM.isHTMLElement(container.current)) allContainers.add(container.current)
 
       let relatedTarget = e.relatedTarget
-      if (!(relatedTarget instanceof HTMLElement)) return
+      if (!DOM.isHTMLorSVGElement(relatedTarget)) return
 
       // Known guards, leave them alone!
       if (relatedTarget.dataset.headlessuiFocusGuard === 'true') {
@@ -190,7 +191,7 @@ function FocusTrapFn<TTag extends ElementType = typeof DEFAULT_FOCUS_TRAP_TAG>(
 
         // It was invoked via something else (e.g.: click, programmatically, ...). Redirect to the
         // previous active item in the FocusTrap
-        else if (e.target instanceof HTMLElement) {
+        else if (DOM.isHTMLorSVGElement(e.target)) {
           focusElement(e.target)
         }
       }
@@ -247,7 +248,7 @@ export let FocusTrap = Object.assign(FocusTrapRoot, {
 // ---
 
 function useRestoreElement(enabled: boolean = true) {
-  let localHistory = useRef<HTMLElement[]>(history.slice())
+  let localHistory = useRef(history.slice())
 
   useWatch(
     ([newEnabled], [oldEnabled]) => {
@@ -418,7 +419,7 @@ function useFocusLock(
     ownerDocument: Document | null
     container: MutableRefObject<HTMLElement | null>
     containers?: Containers
-    previousActiveElement: MutableRefObject<HTMLElement | null>
+    previousActiveElement: MutableRefObject<HTMLOrSVGElement | null>
   }
 ) {
   let mounted = useIsMounted()
@@ -433,14 +434,14 @@ function useFocusLock(
       if (!mounted.current) return
 
       let allContainers = resolveContainers(containers)
-      if (container.current instanceof HTMLElement) allContainers.add(container.current)
+      if (DOM.isHTMLElement(container.current)) allContainers.add(container.current)
 
       let previous = previousActiveElement.current
       if (!previous) return
 
       let toElement = event.target as HTMLElement | null
 
-      if (toElement && toElement instanceof HTMLElement) {
+      if (DOM.isHTMLElement(toElement)) {
         if (!contains(allContainers, toElement)) {
           event.preventDefault()
           event.stopPropagation()
@@ -457,7 +458,7 @@ function useFocusLock(
   )
 }
 
-function contains(containers: Set<HTMLElement>, element: HTMLElement) {
+function contains(containers: Set<Element>, element: Element) {
   for (let container of containers) {
     if (container.contains(element)) return true
   }
diff --git a/packages/@headlessui-react/src/components/label/label.tsx b/packages/@headlessui-react/src/components/label/label.tsx
index d6a970c..620daed 100644
--- a/packages/@headlessui-react/src/components/label/label.tsx
+++ b/packages/@headlessui-react/src/components/label/label.tsx
@@ -155,7 +155,7 @@ function LabelFn<TTag extends ElementType = typeof DEFAULT_LABEL_TAG>(
     // Labels connected to 'real' controls will already click the element. But we don't know that
     // ahead of time. This will prevent the default click, such that only a single click happens
     // instead of two. Otherwise this results in a visual no-op.
-    if (current instanceof HTMLLabelElement) {
+    if (DOM.isHTMLLabelElement(current)) {
       e.preventDefault()
     }
 
@@ -168,7 +168,7 @@ function LabelFn<TTag extends ElementType = typeof DEFAULT_LABEL_TAG>(
       context.props.onClick(e)
     }
 
-    if (current instanceof HTMLLabelElement) {
+    if (DOM.isHTMLLabelElement(current)) {
       let target = document.getElementById(current.htmlFor)
       if (target) {
         // Bail if the target element is disabled
@@ -186,7 +186,7 @@ function LabelFn<TTag extends ElementType = typeof DEFAULT_LABEL_TAG>(
         // immediately require state changes, e.g.: Radio & Checkbox inputs need to be checked (or
         // unchecked).
         if (
-          (target instanceof HTMLInputElement &&
+          (DOM.isHTMLInputElement(target) &&
             (target.type === 'file' || target.type === 'radio' || target.type === 'checkbox')) ||
           target.role === 'radio' ||
           target.role === 'checkbox' ||
diff --git a/packages/@headlessui-react/src/components/listbox/listbox.tsx b/packages/@headlessui-react/src/components/listbox/listbox.tsx
index 78f9c3a..a5a1833 100644
--- a/packages/@headlessui-react/src/components/listbox/listbox.tsx
+++ b/packages/@headlessui-react/src/components/listbox/listbox.tsx
@@ -60,6 +60,7 @@ import type { EnsureArray, Props } from '../../types'
 import { isDisabledReactIssue7711 } from '../../utils/bugs'
 import { Focus } from '../../utils/calculate-active-index'
 import { disposables } from '../../utils/disposables'
+import * as DOM from '../../utils/dom'
 import {
   Focus as FocusManagementFocus,
   FocusableMode,
@@ -376,8 +377,8 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
         }
 
         let option = e.target.closest('[role="option"]:not([data-disabled])')
-        if (option !== null) {
-          return QuickReleaseAction.Select(option as HTMLElement)
+        if (DOM.isHTMLElement(option)) {
+          return QuickReleaseAction.Select(option)
         }
 
         if (optionsElement?.contains(e.target)) {
diff --git a/packages/@headlessui-react/src/components/menu/menu.tsx b/packages/@headlessui-react/src/components/menu/menu.tsx
index 4ad25b5..da1c262 100644
--- a/packages/@headlessui-react/src/components/menu/menu.tsx
+++ b/packages/@headlessui-react/src/components/menu/menu.tsx
@@ -51,6 +51,7 @@ import type { Props } from '../../types'
 import { isDisabledReactIssue7711 } from '../../utils/bugs'
 import { Focus } from '../../utils/calculate-active-index'
 import { disposables } from '../../utils/disposables'
+import * as DOM from '../../utils/dom'
 import {
   Focus as FocusManagementFocus,
   FocusableMode,
@@ -241,8 +242,8 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
         }
 
         let item = e.target.closest('[role="menuitem"]:not([data-disabled])')
-        if (item !== null) {
-          return QuickReleaseAction.Select(item as HTMLElement)
+        if (DOM.isHTMLElement(item)) {
+          return QuickReleaseAction.Select(item)
         }
 
         if (itemsElement?.contains(e.target)) {
diff --git a/packages/@headlessui-react/src/components/popover/popover.tsx b/packages/@headlessui-react/src/components/popover/popover.tsx
index 68e11ca..8876546 100644
--- a/packages/@headlessui-react/src/components/popover/popover.tsx
+++ b/packages/@headlessui-react/src/components/popover/popover.tsx
@@ -59,6 +59,7 @@ import {
 } from '../../internal/open-closed'
 import type { Props } from '../../types'
 import { isDisabledReactIssue7711 } from '../../utils/bugs'
+import * as DOM from '../../utils/dom'
 import {
   Focus,
   FocusResult,
@@ -358,7 +359,7 @@ function PopoverFn<TTag extends ElementType = typeof DEFAULT_POPOVER_TAG>(
     'focus',
     (event) => {
       if (event.target === window) return
-      if (!(event.target instanceof HTMLElement)) return
+      if (!DOM.isHTMLorSVGElement(event.target)) return
       if (popoverState !== PopoverStates.Open) return
       if (isFocusWithinPopoverGroup()) return
       if (!button) return
@@ -395,8 +396,8 @@ function PopoverFn<TTag extends ElementType = typeof DEFAULT_POPOVER_TAG>(
 
       let restoreElement = (() => {
         if (!focusableElement) return button
-        if (focusableElement instanceof HTMLElement) return focusableElement
-        if ('current' in focusableElement && focusableElement.current instanceof HTMLElement)
+        if (DOM.isHTMLElement(focusableElement)) return focusableElement
+        if ('current' in focusableElement && DOM.isHTMLElement(focusableElement.current))
           return focusableElement.current
 
         return button
@@ -679,8 +680,8 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
 
   let direction = useTabDirection()
   let handleFocus = useEvent(() => {
-    let el = state.panel as HTMLElement
-    if (!el) return
+    if (!DOM.isHTMLElement(state.panel)) return
+    let el = state.panel
 
     function run() {
       let result = match(direction.current, {
diff --git a/packages/@headlessui-react/src/components/portal/portal.tsx b/packages/@headlessui-react/src/components/portal/portal.tsx
index 8c4a6d7..9a21fb0 100644
--- a/packages/@headlessui-react/src/components/portal/portal.tsx
+++ b/packages/@headlessui-react/src/components/portal/portal.tsx
@@ -22,6 +22,7 @@ import { useServerHandoffComplete } from '../../hooks/use-server-handoff-complet
 import { optionalRef, useSyncRefs } from '../../hooks/use-sync-refs'
 import { usePortalRoot } from '../../internal/portal-force-root'
 import type { Props } from '../../types'
+import * as DOM from '../../utils/dom'
 import { env } from '../../utils/env'
 import { forwardRefWithAs, useRender, type HasDisplayName, type RefProp } from '../../utils/render'
 
@@ -120,7 +121,7 @@ let InternalPortalFn = forwardRefWithAs(function InternalPortalFn<
   useOnUnmount(() => {
     if (!target || !element) return
 
-    if (element instanceof Node && target.contains(element)) {
+    if (DOM.isNode(element) && target.contains(element)) {
       target.removeChild(element)
     }
 
diff --git a/packages/@headlessui-react/src/components/switch/switch.tsx b/packages/@headlessui-react/src/components/switch/switch.tsx
index f3c9df6..6076dc4 100644
--- a/packages/@headlessui-react/src/components/switch/switch.tsx
+++ b/packages/@headlessui-react/src/components/switch/switch.tsx
@@ -28,6 +28,7 @@ import { FormFields } from '../../internal/form-fields'
 import { useProvidedId } from '../../internal/id'
 import type { Props } from '../../types'
 import { isDisabledReactIssue7711 } from '../../utils/bugs'
+import * as DOM from '../../utils/dom'
 import { attemptSubmit } from '../../utils/form'
 import {
   forwardRefWithAs,
@@ -85,7 +86,7 @@ function GroupFn<TTag extends ElementType = typeof DEFAULT_GROUP_TAG>(
           htmlFor: context.switch?.id,
           onClick(event: React.MouseEvent<HTMLLabelElement>) {
             if (!switchElement) return
-            if (event.currentTarget instanceof HTMLLabelElement) {
+            if (DOM.isHTMLLabelElement(event.currentTarget)) {
               event.preventDefault()
             }
             switchElement.click()
diff --git a/packages/@headlessui-react/src/hooks/document-overflow/handle-ios-locking.ts b/packages/@headlessui-react/src/hooks/document-overflow/handle-ios-locking.ts
index 3a70893..19404eb 100644
--- a/packages/@headlessui-react/src/hooks/document-overflow/handle-ios-locking.ts
+++ b/packages/@headlessui-react/src/hooks/document-overflow/handle-ios-locking.ts
@@ -1,4 +1,5 @@
 import { disposables } from '../../utils/disposables'
+import * as DOM from '../../utils/dom'
 import { isIOS } from '../../utils/platform'
 import type { ScrollLockStep } from './overflow-store'
 
@@ -13,7 +14,7 @@ export function handleIOSLocking(): ScrollLockStep<ContainerMetadata> {
 
   return {
     before({ doc, d, meta }) {
-      function inAllowedContainer(el: HTMLElement) {
+      function inAllowedContainer(el: Element) {
         return meta.containers
           .flatMap((resolve) => resolve())
           .some((container) => container.contains(el))
@@ -46,12 +47,12 @@ export function handleIOSLocking(): ScrollLockStep<ContainerMetadata> {
         //
         // Let's try and capture that element and store it, so that we can later scroll to it once the
         // Dialog closes.
-        let scrollToElement: HTMLElement | null = null
+        let scrollToElement: Element | null = null
         d.addEventListener(
           doc,
           'click',
           (e) => {
-            if (!(e.target instanceof HTMLElement)) {
+            if (!DOM.isHTMLorSVGElement(e.target)) {
               return
             }
 
@@ -60,8 +61,8 @@ export function handleIOSLocking(): ScrollLockStep<ContainerMetadata> {
               if (!anchor) return
               let { hash } = new URL(anchor.href)
               let el = doc.querySelector(hash)
-              if (el && !inAllowedContainer(el as HTMLElement)) {
-                scrollToElement = el as HTMLElement
+              if (DOM.isHTMLorSVGElement(el) && !inAllowedContainer(el)) {
+                scrollToElement = el
               }
             } catch (err) {}
           },
@@ -70,8 +71,8 @@ export function handleIOSLocking(): ScrollLockStep<ContainerMetadata> {
 
         // Rely on overscrollBehavior to prevent scrolling outside of the Dialog.
         d.addEventListener(doc, 'touchstart', (e) => {
-          if (e.target instanceof HTMLElement) {
-            if (inAllowedContainer(e.target as HTMLElement)) {
+          if (DOM.isHTMLorSVGElement(e.target) && DOM.hasInlineStyle(e.target)) {
+            if (inAllowedContainer(e.target)) {
               // Find the root of the allowed containers
               let rootContainer = e.target
               while (
@@ -93,14 +94,14 @@ export function handleIOSLocking(): ScrollLockStep<ContainerMetadata> {
           'touchmove',
           (e) => {
             // Check if we are scrolling inside any of the allowed containers, if not let's cancel the event!
-            if (e.target instanceof HTMLElement) {
+            if (DOM.isHTMLorSVGElement(e.target)) {
               // Some inputs like `<input type=range>` use touch events to
               // allow interaction. We should not prevent this event.
-              if (e.target.tagName === 'INPUT') {
+              if (DOM.isHTMLInputElement(e.target)) {
                 return
               }
 
-              if (inAllowedContainer(e.target as HTMLElement)) {
+              if (inAllowedContainer(e.target)) {
                 // Even if we are in an allowed container, on iOS the main page can still scroll, we
                 // have to make sure that we `event.preventDefault()` this event to prevent that.
                 //
diff --git a/packages/@headlessui-react/src/hooks/use-on-disappear.ts b/packages/@headlessui-react/src/hooks/use-on-disappear.ts
index 924a755..6ac4bb3 100644
--- a/packages/@headlessui-react/src/hooks/use-on-disappear.ts
+++ b/packages/@headlessui-react/src/hooks/use-on-disappear.ts
@@ -1,5 +1,6 @@
 import { useEffect, type MutableRefObject } from 'react'
 import { disposables } from '../utils/disposables'
+import * as DOM from '../utils/dom'
 import { useLatestValue } from './use-latest-value'
 
 /**
@@ -24,21 +25,21 @@ export function useOnDisappear(
   useEffect(() => {
     if (!enabled) return
 
-    let element = ref === null ? null : ref instanceof HTMLElement ? ref : ref.current
+    let element = ref === null ? null : DOM.isHTMLElement(ref) ? ref : ref.current
     if (!element) return
 
     let d = disposables()
 
     // Try using ResizeObserver
     if (typeof ResizeObserver !== 'undefined') {
-      let observer = new ResizeObserver(() => listenerRef.current(element))
+      let observer = new ResizeObserver(() => listenerRef.current(element!))
       observer.observe(element)
       d.add(() => observer.disconnect())
     }
 
     // Try using IntersectionObserver
     if (typeof IntersectionObserver !== 'undefined') {
-      let observer = new IntersectionObserver(() => listenerRef.current(element))
+      let observer = new IntersectionObserver(() => listenerRef.current(element!))
       observer.observe(element)
       d.add(() => observer.disconnect())
     }
diff --git a/packages/@headlessui-react/src/hooks/use-outside-click.ts b/packages/@headlessui-react/src/hooks/use-outside-click.ts
index 43d47d0..415befa 100644
--- a/packages/@headlessui-react/src/hooks/use-outside-click.ts
+++ b/packages/@headlessui-react/src/hooks/use-outside-click.ts
@@ -1,4 +1,5 @@
 import { useCallback, useRef } from 'react'
+import * as DOM from '../utils/dom'
 import { FocusableMode, isFocusableElement } from '../utils/focus-management'
 import { isMobile } from '../utils/platform'
 import { useDocumentEvent } from './use-document-event'
@@ -21,7 +22,10 @@ const MOVE_THRESHOLD_PX = 30
 export function useOutsideClick(
   enabled: boolean,
   containers: ContainerInput | (() => ContainerInput),
-  cb: (event: MouseEvent | PointerEvent | FocusEvent | TouchEvent, target: HTMLElement) => void
+  cb: (
+    event: MouseEvent | PointerEvent | FocusEvent | TouchEvent,
+    target: HTMLOrSVGElement & Element
+  ) => void
 ) {
   let isTopLayer = useIsTopLayer(enabled, 'outside-click')
   let cbRef = useLatestValue(cb)
@@ -29,7 +33,7 @@ export function useOutsideClick(
   let handleOutsideClick = useCallback(
     function handleOutsideClick<E extends MouseEvent | PointerEvent | FocusEvent | TouchEvent>(
       event: E,
-      resolveTarget: (event: E) => HTMLElement | null
+      resolveTarget: (event: E) => (HTMLOrSVGElement & Element) | null
     ) {
       // Check whether the event got prevented already. This can happen if you
       // use the useOutsideClick hook in both a Dialog and a Menu and the inner
@@ -175,7 +179,7 @@ export function useOutsideClick(
       }
 
       return handleOutsideClick(event, () => {
-        if (event.target instanceof HTMLElement) {
+        if (DOM.isHTMLorSVGElement(event.target)) {
           return event.target
         }
         return null
@@ -201,7 +205,7 @@ export function useOutsideClick(
     'blur',
     (event) => {
       return handleOutsideClick(event, () => {
-        return window.document.activeElement instanceof HTMLIFrameElement
+        return DOM.isHTMLIframeElement(window.document.activeElement)
           ? window.document.activeElement
           : null
       })
diff --git a/packages/@headlessui-react/src/hooks/use-quick-release.ts b/packages/@headlessui-react/src/hooks/use-quick-release.ts
index 2b99591..a0d65c3 100644
--- a/packages/@headlessui-react/src/hooks/use-quick-release.ts
+++ b/packages/@headlessui-react/src/hooks/use-quick-release.ts
@@ -66,7 +66,7 @@ export function useQuickRelease(
     'pointerup',
     (e) => {
       if (triggeredAtRef.current === null) return
-      if (!DOM.isHTMLElement(e.target)) return
+      if (!DOM.isHTMLorSVGElement(e.target)) return
 
       let result = action(e as PointerEventWithTarget)
 
diff --git a/packages/@headlessui-react/src/hooks/use-refocusable-input.ts b/packages/@headlessui-react/src/hooks/use-refocusable-input.ts
index e8e361b..9021ec0 100644
--- a/packages/@headlessui-react/src/hooks/use-refocusable-input.ts
+++ b/packages/@headlessui-react/src/hooks/use-refocusable-input.ts
@@ -1,4 +1,5 @@
 import { useRef } from 'react'
+import * as DOM from '../utils/dom'
 import { useEvent } from './use-event'
 import { useEventListener } from './use-event-listener'
 
@@ -18,7 +19,7 @@ export function useRefocusableInput(input: HTMLInputElement | null) {
 
   useEventListener(input, 'blur', (event) => {
     let target = event.target
-    if (!(target instanceof HTMLInputElement)) return
+    if (!DOM.isHTMLInputElement(target)) return
 
     info.current = {
       value: target.value,
@@ -31,7 +32,7 @@ export function useRefocusableInput(input: HTMLInputElement | null) {
     // If the input is already focused, we don't need to do anything
     if (document.activeElement === input) return
 
-    if (!(input instanceof HTMLInputElement)) return
+    if (!DOM.isHTMLInputElement(input)) return
     if (!input.isConnected) return
 
     // Focus the input
diff --git a/packages/@headlessui-react/src/hooks/use-resolved-tag.ts b/packages/@headlessui-react/src/hooks/use-resolved-tag.ts
index ce09066..adebc40 100644
--- a/packages/@headlessui-react/src/hooks/use-resolved-tag.ts
+++ b/packages/@headlessui-react/src/hooks/use-resolved-tag.ts
@@ -1,4 +1,5 @@
 import { useCallback, useState } from 'react'
+import * as DOM from '../utils/dom'
 
 /**
  * Resolve the actual rendered tag of a DOM node. If the `tag` provided is
@@ -22,7 +23,7 @@ export function useResolvedTag<T extends React.ElementType>(tag: T) {
         // Tag name is already known and it's a string, no need to re-render
         if (tagName) return
 
-        if (ref instanceof HTMLElement) {
+        if (DOM.isHTMLElement(ref)) {
           // Tag name is not known yet, render the component to find out
           setResolvedTag(ref.tagName.toLowerCase())
         }
diff --git a/packages/@headlessui-react/src/hooks/use-root-containers.tsx b/packages/@headlessui-react/src/hooks/use-root-containers.tsx
index cf4fded..ecbe7e8 100644
--- a/packages/@headlessui-react/src/hooks/use-root-containers.tsx
+++ b/packages/@headlessui-react/src/hooks/use-root-containers.tsx
@@ -1,5 +1,6 @@
 import React, { createContext, useContext, useState, type MutableRefObject } from 'react'
 import { Hidden, HiddenFeatures } from '../internal/hidden'
+import * as DOM from '../utils/dom'
 import { getOwnerDocument } from '../utils/owner'
 import { useEvent } from './use-event'
 import { useOwnerDocument } from './use-owner'
@@ -11,21 +12,21 @@ export function useRootContainers({
   // Reference to a node in the "main" tree, not in the portalled Dialog tree.
   mainTreeNode,
 }: {
-  defaultContainers?: (HTMLElement | null | MutableRefObject<HTMLElement | null>)[]
-  portals?: MutableRefObject<HTMLElement[]>
-  mainTreeNode?: HTMLElement | null
+  defaultContainers?: (Element | null | MutableRefObject<Element | null>)[]
+  portals?: MutableRefObject<Element[]>
+  mainTreeNode?: Element | null
 } = {}) {
   let ownerDocument = useOwnerDocument(mainTreeNode)
 
   let resolveContainers = useEvent(() => {
-    let containers: HTMLElement[] = []
+    let containers: Element[] = []
 
     // Resolve default containers
     for (let container of defaultContainers) {
       if (container === null) continue
-      if (container instanceof HTMLElement) {
+      if (DOM.isElement(container)) {
         containers.push(container)
-      } else if ('current' in container && container.current instanceof HTMLElement) {
+      } else if ('current' in container && DOM.isElement(container.current)) {
         containers.push(container.current)
       }
     }
@@ -41,7 +42,7 @@ export function useRootContainers({
     for (let container of ownerDocument?.querySelectorAll('html > *, body > *') ?? []) {
       if (container === document.body) continue // Skip `<body>`
       if (container === document.head) continue // Skip `<head>`
-      if (!(container instanceof HTMLElement)) continue // Skip non-HTMLElements
+      if (!DOM.isElement(container)) continue // Skip non-HTMLElements
       if (container.id === 'headlessui-portal-root') continue // Skip the Headless UI portal root
       if (mainTreeNode) {
         if (container.contains(mainTreeNode)) continue // Skip if it is the main app
@@ -57,13 +58,13 @@ export function useRootContainers({
 
   return {
     resolveContainers,
-    contains: useEvent((element: HTMLElement) =>
+    contains: useEvent((element: Element) =>
       resolveContainers().some((container) => container.contains(element))
     ),
   }
 }
 
-let MainTreeContext = createContext<HTMLElement | null>(null)
+let MainTreeContext = createContext<Element | null>(null)
 
 /**
  * A provider for the main tree node.
@@ -93,9 +94,9 @@ export function MainTreeProvider({
   node,
 }: {
   children: React.ReactNode
-  node?: HTMLElement | null
+  node?: Element | null
 }) {
-  let [mainTreeNode, setMainTreeNode] = useState<HTMLElement | null>(null)
+  let [mainTreeNode, setMainTreeNode] = useState<Element | null>(null)
 
   // 1. Prefer the main tree node from context
   // 2. Prefer the provided node
@@ -126,7 +127,7 @@ export function MainTreeProvider({
               []) {
               if (container === document.body) continue // Skip `<body>`
               if (container === document.head) continue // Skip `<head>`
-              if (!(container instanceof HTMLElement)) continue // Skip non-HTMLElements
+              if (!DOM.isElement(container)) continue // Skip non-HTMLElements
               if (container?.contains(el)) {
                 setMainTreeNode(container)
                 break
@@ -142,7 +143,7 @@ export function MainTreeProvider({
 /**
  * Get the main tree node from context or fallback to the optionally provided node.
  */
-export function useMainTreeNode(fallbackMainTreeNode: HTMLElement | null = null) {
+export function useMainTreeNode(fallbackMainTreeNode: Element | null = null) {
   // Prefer the main tree node from context, but fallback to the provided node.
   return useContext(MainTreeContext) ?? fallbackMainTreeNode
 }
diff --git a/packages/@headlessui-react/src/internal/floating.tsx b/packages/@headlessui-react/src/internal/floating.tsx
index cc611d6..66f22f3 100644
--- a/packages/@headlessui-react/src/internal/floating.tsx
+++ b/packages/@headlessui-react/src/internal/floating.tsx
@@ -16,9 +16,12 @@ import { createContext, useCallback, useContext, useMemo, useRef, useState } fro
 import { useDisposables } from '../hooks/use-disposables'
 import { useEvent } from '../hooks/use-event'
 import { useIsoMorphicEffect } from '../hooks/use-iso-morphic-effect'
+import * as DOM from '../utils/dom'
 
 type Align = 'start' | 'end'
 type Placement = 'top' | 'right' | 'bottom' | 'left'
+type AnchorTo = `${Placement}` | `${Placement} ${Align}`
+type AnchorToWithSelection = `${Placement | 'selection'}` | `${Placement | 'selection'} ${Align}`
 
 type BaseAnchorProps = {
   /**
@@ -39,27 +42,27 @@ type BaseAnchorProps = {
 
 export type AnchorProps =
   | false // Disable entirely
-  | (`${Placement}` | `${Placement} ${Align}`) // String value to define the placement
+  | AnchorTo // String value to define the placement
   | Partial<
       BaseAnchorProps & {
         /**
          * The `to` value defines which side of the trigger the panel should be placed on and its
          * alignment.
          */
-        to: `${Placement}` | `${Placement} ${Align}`
+        to: AnchorTo
       }
     >
 
 export type AnchorPropsWithSelection =
   | false // Disable entirely
-  | (`${Placement | 'selection'}` | `${Placement | 'selection'} ${Align}`)
+  | AnchorToWithSelection
   | Partial<
       BaseAnchorProps & {
         /**
          * The `to` value defines which side of the trigger the panel should be placed on and its
          * alignment.
          */
-        to: `${Placement | 'selection'}` | `${Placement | 'selection'} ${Align}`
+        to: AnchorToWithSelection
       }
     >
 
@@ -77,7 +80,7 @@ let FloatingContext = createContext<{
   getReferenceProps: ReturnType<typeof useInteractions>['getReferenceProps']
   getFloatingProps: ReturnType<typeof useInteractions>['getFloatingProps']
   slot: Partial<{
-    anchor: `${Placement | 'selection'}` | `${Placement | 'selection'} ${Align}`
+    anchor: AnchorToWithSelection
   }>
 }>({
   styles: undefined,
@@ -258,7 +261,7 @@ export function FloatingProvider({
               let elementAmountVisible = 0
 
               for (let child of context.elements.floating?.childNodes ?? []) {
-                if (child instanceof HTMLElement) {
+                if (DOM.isHTMLElement(child)) {
                   let childTop = child.offsetTop
                   // It can be that the child is fully visible, but we also want to keep the scroll
                   // padding into account to ensure the UI looks good. Therefore we fake that the
diff --git a/packages/@headlessui-react/src/utils/active-element-history.ts b/packages/@headlessui-react/src/utils/active-element-history.ts
index 1b66e06..8a9af98 100644
--- a/packages/@headlessui-react/src/utils/active-element-history.ts
+++ b/packages/@headlessui-react/src/utils/active-element-history.ts
@@ -1,14 +1,15 @@
 import { onDocumentReady } from './document-ready'
+import * as DOM from './dom'
 import { focusableSelector } from './focus-management'
 
-export let history: HTMLElement[] = []
+export let history: (HTMLOrSVGElement & Element)[] = []
 onDocumentReady(() => {
   function handle(e: Event) {
-    if (!(e.target instanceof HTMLElement)) return
+    if (!DOM.isHTMLorSVGElement(e.target)) return
     if (e.target === document.body) return
     if (history[0] === e.target) return
 
-    let focusableElement = e.target as HTMLElement
+    let focusableElement = e.target
 
     // Figure out the closest focusable element, this is needed in a situation
     // where you click on a non-focusable element inside a focusable element.
@@ -20,7 +21,7 @@ onDocumentReady(() => {
     //   <span>Click me</span>
     // </button>
     // ```
-    focusableElement = focusableElement.closest(focusableSelector) as HTMLElement
+    focusableElement = focusableElement.closest(focusableSelector) as HTMLOrSVGElement & Element
 
     history.unshift(focusableElement ?? e.target)
 
diff --git a/packages/@headlessui-react/src/utils/bugs.ts b/packages/@headlessui-react/src/utils/bugs.ts
index 7120477..1c506af 100644
--- a/packages/@headlessui-react/src/utils/bugs.ts
+++ b/packages/@headlessui-react/src/utils/bugs.ts
@@ -1,3 +1,5 @@
+import * as DOM from './dom'
+
 // See: https://github.com/facebook/react/issues/7711
 // See: https://github.com/facebook/react/pull/20612
 // See: https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#concept-fe-disabled (2.)
@@ -5,8 +7,8 @@ export function isDisabledReactIssue7711(element: Element): boolean {
   let parent = element.parentElement
   let legend = null
 
-  while (parent && !(parent instanceof HTMLFieldSetElement)) {
-    if (parent instanceof HTMLLegendElement) legend = parent
+  while (parent && !DOM.isHTMLFieldSetElement(parent)) {
+    if (DOM.isHTMLLegendElement(parent)) legend = parent
     parent = parent.parentElement
   }
 
@@ -22,7 +24,7 @@ function isFirstLegend(element: HTMLLegendElement | null): boolean {
   let previous = element.previousElementSibling
 
   while (previous !== null) {
-    if (previous instanceof HTMLLegendElement) return false
+    if (DOM.isHTMLLegendElement(previous)) return false
     previous = previous.previousElementSibling
   }
 
diff --git a/packages/@headlessui-react/src/utils/disposables.ts b/packages/@headlessui-react/src/utils/disposables.ts
index 3b404e5..8b7fc01 100644
--- a/packages/@headlessui-react/src/utils/disposables.ts
+++ b/packages/@headlessui-react/src/utils/disposables.ts
@@ -56,7 +56,7 @@ export function disposables() {
       })
     },
 
-    style(node: HTMLElement, property: string, value: string) {
+    style(node: ElementCSSInlineStyle, property: string, value: string) {
       let previous = node.style.getPropertyValue(property)
       Object.assign(node.style, { [property]: value })
       return this.add(() => {
diff --git a/packages/@headlessui-react/src/utils/dom.ts b/packages/@headlessui-react/src/utils/dom.ts
index 481a580..aae4ef3 100644
--- a/packages/@headlessui-react/src/utils/dom.ts
+++ b/packages/@headlessui-react/src/utils/dom.ts
@@ -1,7 +1,7 @@
 // This file contains a bunch of utilities to verify that an element is of a
 // specific type.
 //
-// Normally you can use `elemenent instanceof HTMLElement`, but if you are in
+// Normally you can use `element instanceof HTMLElement`, but if you are in
 // different JS Context (e.g.: inside an iframe) then the `HTMLElement` will be
 // a different class and the check will fail.
 //
@@ -11,13 +11,52 @@
 export function isNode(element: unknown): element is Node {
   if (typeof element !== 'object') return false
   if (element === null) return false
-  return 'nodeType' in element && 'nodeName' in element
+  return 'nodeType' in element
+}
+
+export function isElement(element: unknown): element is Element {
+  return isNode(element) && 'tagName' in element
 }
 
 export function isHTMLElement(element: unknown): element is HTMLElement {
-  if (typeof element !== 'object') return false
-  if (element === null) return false
-  return 'nodeName' in element
+  return isElement(element) && 'accessKey' in element
+}
+
+// HTMLOrSVGElement doesn't inherit from HTMLElement or from Element. But this
+// is the type that contains the `tabIndex` property.
+//
+// Once we know that this is an `HTMLOrSVGElement` we also know that it is an
+// `Element` (that contains more information)
+export function isHTMLorSVGElement(element: unknown): element is HTMLOrSVGElement & Element {
+  return isElement(element) && 'tabIndex' in element
+}
+
+export function hasInlineStyle(element: unknown): element is ElementCSSInlineStyle {
+  return isElement(element) && 'style' in element
+}
+
+export function isHTMLIframeElement(element: unknown): element is HTMLIFrameElement {
+  return isHTMLElement(element) && element.nodeName === 'IFRAME'
+}
+
+export function isHTMLInputElement(element: unknown): element is HTMLInputElement {
+  return isHTMLElement(element) && element.nodeName === 'INPUT'
+}
+
+export function isHTMLTextAreaElement(element: unknown): element is HTMLTextAreaElement {
+  return isHTMLElement(element) && element.nodeName === 'TEXTAREA'
+}
+
+export function isHTMLLabelElement(element: unknown): element is HTMLLabelElement {
+  return isHTMLElement(element) && element.nodeName === 'LABEL'
+}
+
+export function isHTMLFieldSetElement(element: unknown): element is HTMLFieldSetElement {
+  return isHTMLElement(element) && element.nodeName === 'FIELDSET'
+}
+
+export function isHTMLLegendElement(element: unknown): element is HTMLLegendElement {
+  return isHTMLElement(element) && element.nodeName === 'LEGEND'
 }
 
 // https://html.spec.whatwg.org/#interactive-content-2
@@ -34,7 +73,7 @@ export function isHTMLElement(element: unknown): element is HTMLElement {
 // - textarea
 // - video (if the controls attribute is present)
 export function isInteractiveElement(element: unknown): element is Element {
-  if (!isHTMLElement(element)) return false
+  if (!isElement(element)) return false
 
   return element.matches(
     'a[href],audio[controls],button,details,embed,iframe,img[usemap],input:not([type="hidden"]),label,select,textarea,video[controls]'
diff --git a/packages/@headlessui-react/src/utils/focus-management.ts b/packages/@headlessui-react/src/utils/focus-management.ts
index 1a39a3b..bbc2043 100644
--- a/packages/@headlessui-react/src/utils/focus-management.ts
+++ b/packages/@headlessui-react/src/utils/focus-management.ts
@@ -1,5 +1,6 @@
 import type { MutableRefObject } from 'react'
 import { disposables } from './disposables'
+import * as DOM from './dom'
 import { match } from './match'
 import { getOwnerDocument } from './owner'
 
@@ -109,7 +110,7 @@ export enum FocusableMode {
 }
 
 export function isFocusableElement(
-  element: HTMLElement,
+  element: HTMLOrSVGElement & Element,
   mode: FocusableMode = FocusableMode.Strict
 ) {
   if (element === getOwnerDocument(element)?.body) return false
@@ -119,7 +120,7 @@ export function isFocusableElement(
       return element.matches(focusableSelector)
     },
     [FocusableMode.Loose]() {
-      let next: HTMLElement | null = element
+      let next: Element | null = element
 
       while (next !== null) {
         if (next.matches(focusableSelector)) return true
@@ -136,7 +137,8 @@ export function restoreFocusIfNecessary(element: HTMLElement | null) {
   disposables().nextFrame(() => {
     if (
       ownerDocument &&
-      !isFocusableElement(ownerDocument.activeElement as HTMLElement, FocusableMode.Strict)
+      DOM.isHTMLorSVGElement(ownerDocument.activeElement) &&
+      !isFocusableElement(ownerDocument.activeElement, FocusableMode.Strict)
     ) {
       focusElement(element)
     }
@@ -184,7 +186,7 @@ if (typeof window !== 'undefined' && typeof document !== 'undefined') {
   )
 }
 
-export function focusElement(element: HTMLElement | null) {
+export function focusElement(element: HTMLOrSVGElement | null) {
   element?.focus({ preventScroll: true })
 }
 
diff --git a/packages/@headlessui-react/src/utils/get-text-value.ts b/packages/@headlessui-react/src/utils/get-text-value.ts
index 59685d8..7d441a7 100644
--- a/packages/@headlessui-react/src/utils/get-text-value.ts
+++ b/packages/@headlessui-react/src/utils/get-text-value.ts
@@ -1,3 +1,5 @@
+import * as DOM from './dom'
+
 let emojiRegex =
   /([\u2700-\u27BF]|[\uE000-\uF8FF]|\uD83C[\uDC00-\uDFFF]|\uD83D[\uDC00-\uDFFF]|[\u2011-\u26FF]|\uD83E[\uDD10-\uDDFF])/g
 
@@ -19,7 +21,7 @@ function getTextContents(element: HTMLElement): string {
   // This is probably the slowest part, but if you want complete control over the text value, then
   // it is better to set an `aria-label` instead.
   let copy = element.cloneNode(true)
-  if (!(copy instanceof HTMLElement)) {
+  if (!DOM.isHTMLElement(copy)) {
     return currentInnerText
   }
 


Test patch:
diff --git a/packages/@headlessui-react/src/test-utils/interactions.ts b/packages/@headlessui-react/src/test-utils/interactions.ts
index 6e2c608..5b04ecf 100644
--- a/packages/@headlessui-react/src/test-utils/interactions.ts
+++ b/packages/@headlessui-react/src/test-utils/interactions.ts
@@ -1,5 +1,6 @@
 import { fireEvent } from '@testing-library/react'
 import { act } from 'react'
+import * as DOM from '../utils/dom'
 import { pointer } from './fake-pointer'
 
 function nextFrame(cb: Function): void {
@@ -41,7 +42,7 @@ export function word(input: string): Partial<KeyboardEvent>[] {
 
   let element = document.activeElement
 
-  if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) {
+  if (DOM.isHTMLInputElement(element) || DOM.isHTMLTextAreaElement(element)) {
     fireEvent.change(element, {
       target: Object.assign({}, element, { value: input }),
     })