Repository: vueuse/vueuse (2026-03-24T01:44:17Z)

Issue:
BUG | `onClickOutside` | Detecting iFrame clicks fails if the iFrame is within a custom element Shadow DOM, even when enabling `detectIframe` option
### Describe the bug

`onClickOutside` doesn't trigger when clicking an iframe which is wrapped in a custom element within a Shadow DOM, even if `detectIframe` is enabled.
This is because `detectIframe` actually verifies that the activeElement is an iFrame so it won't detect clicks in custom elements. I confirmed that the window `blur` event is indeed firing when clicking the iframe, but onClickOutside is not triggering.

Could the check be made broader, to trigger any time the window loses focus?

### Reproduction

https://stackblitz.com/edit/vitejs-vite-jcsqmkbs?file=src%2FApp.vue

### System Info

```Shell
  System:
    OS: Linux 5.0 undefined
    CPU: (8) x64 Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz
    Memory: 0 Bytes / 0 Bytes
    Shell: 1.0 - /bin/jsh
  Binaries:
    Node: 22.22.0 - /usr/local/bin/node
    Yarn: 1.22.19 - /usr/local/bin/yarn
    npm: 10.8.2 - /usr/local/bin/npm
    pnpm: 8.15.6 - /usr/local/bin/pnpm
  npmPackages:
    @vueuse/core: ^14.2.1 => 14.2.1 
    vue: ^3.2.25 => 3.5.30
```

### Used Package Manager

npm

### Validations

- [x] Follow our [Code of Conduct](https://github.com/vueuse/vueuse/blob/main/CODE_OF_CONDUCT.md)
- [x] Read the [Contributing Guidelines](https://github.com/vueuse/vueuse/blob/main/CONTRIBUTING.md).
- [x] Read the [docs](https://vueuse.org/guide).
- [x] Check that there isn't [already an issue](https://github.com/vueuse/vueuse/issues) that reports the same bug to avoid creating a duplicate.
- [x] Make sure this is a VueUse issue and not a framework-specific issue. For example, if it's a Vue SFC related bug, it should likely be reported to https://github.com/vuejs/core instead.
- [x] Check that this is a concrete bug. For Q&A open a [GitHub Discussion](https://github.com/vueuse/vueuse/discussions).
- [x] The provided reproduction is a [minimal reproducible example](https://stackoverflow.com/help/minimal-reproducible-example) of the bug.


Discussion:
@richrd 
Try using the latest version
https://stackblitz.com/edit/vitejs-vite-t5ymv7ag?file=package.json
@babu-ch thanks, I didn't realise I wasn't running the latest version. I have updated to the latest version 14.2.1, and also updated the repro link, along with adding a Shadow DOM which apparently was my issue in the app I'm building. Can you test the new repro? https://stackblitz.com/edit/vitejs-vite-jcsqmkbs?file=src%2FApp.vue,src%2Fmain.js

I have updated the Bug title and description accordingly.



Patch:
diff --git a/packages/core/onClickOutside/index.ts b/packages/core/onClickOutside/index.ts
index edc1450a9f9..9c6a8e70993 100644
--- a/packages/core/onClickOutside/index.ts
+++ b/packages/core/onClickOutside/index.ts
@@ -178,8 +178,11 @@ export function onClickOutside(
     detectIframe && useEventListener(window, 'blur', (event) => {
       setTimeout(() => {
         const el = unrefElement(target)
+        let activeEl: Element | null | undefined = window.document.activeElement
+        while (activeEl?.shadowRoot)
+          activeEl = activeEl.shadowRoot.activeElement
         if (
-          window.document.activeElement?.tagName === 'IFRAME'
+          activeEl?.tagName === 'IFRAME'
           && !el?.contains(window.document.activeElement)
         ) {
           handler(event as any)


Test patch:
diff --git a/packages/core/onClickOutside/index.browser.test.ts b/packages/core/onClickOutside/index.browser.test.ts
index f177b6abe40..5178a31b34b 100644
--- a/packages/core/onClickOutside/index.browser.test.ts
+++ b/packages/core/onClickOutside/index.browser.test.ts
@@ -1,4 +1,4 @@
-import { describe, expect, it, vi } from 'vitest'
+import { beforeEach, describe, expect, it, vi } from 'vitest'
 import { page, userEvent } from 'vitest/browser'
 import { defineComponent, useTemplateRef } from 'vue'
 import { onClickOutside } from './index'
@@ -46,6 +46,10 @@ function getComplexComponent(useGetter = false) {
 }
 
 describe('onClickOutside', () => {
+  beforeEach(() => {
+    document.body.innerHTML = ''
+  })
+
   it('should work with ignored element', async () => {
     const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
     const screen = page.render(getComplexComponent())
@@ -65,6 +69,65 @@ describe('onClickOutside', () => {
     expect(consoleSpy).toHaveBeenCalled()
   })
 
+  it('should detect iframe inside shadow DOM with detectIframe option', async () => {
+    const handler = vi.fn()
+
+    const component = defineComponent({
+      template: `<div ref="target">Inside</div>`,
+      setup() {
+        const target = useTemplateRef<HTMLDivElement>('target')
+        onClickOutside(target, handler, { detectIframe: true })
+        return { target }
+      },
+    })
+
+    const screen = page.render(component)
+    await expect.element(screen.getByText('Inside')).toBeInTheDocument()
+
+    const host = document.createElement('div')
+    const shadowRoot = host.attachShadow({ mode: 'open' })
+    const iframe = document.createElement('iframe')
+    shadowRoot.appendChild(iframe)
+    document.body.appendChild(host)
+
+    await userEvent.click(iframe)
+    await vi.waitFor(() => {
+      expect(handler).toHaveBeenCalledOnce()
+    })
+  })
+
+  it('should detect iframe inside nested shadow DOM with detectIframe option', async () => {
+    const handler = vi.fn()
+
+    const component = defineComponent({
+      template: `<div ref="target">Inside</div>`,
+      setup() {
+        const target = useTemplateRef<HTMLDivElement>('target')
+        onClickOutside(target, handler, { detectIframe: true })
+        return { target }
+      },
+    })
+
+    const screen = page.render(component)
+    await expect.element(screen.getByText('Inside')).toBeInTheDocument()
+
+    // <outer-host> > shadow-root > <inner-host> > shadow-root > <iframe>
+    const outerHost = document.createElement('div')
+    const outerShadow = outerHost.attachShadow({ mode: 'open' })
+    const innerHost = document.createElement('div')
+    const innerShadow = innerHost.attachShadow({ mode: 'open' })
+    const iframe = document.createElement('iframe')
+    innerShadow.appendChild(iframe)
+    outerShadow.appendChild(innerHost)
+    document.body.appendChild(outerHost)
+
+    // Playwright cannot click an iframe nested 2 levels deep in shadow DOM
+    iframe.focus()
+    await vi.waitFor(() => {
+      expect(handler).toHaveBeenCalledOnce()
+    })
+  })
+
   it('allow the value of target to be a getter', async () => {
     const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
     const screen = page.render(getComplexComponent(true))