on-click-outside
Call a handler when the user clicks (or focuses an iframe) outside an element.
| Package | @vizejs/composable/on-click-outside |
| Own the source | vize lib pull composable:on-click-outside |
| Runtime exports | onClickOutside |
| Gzip budget | 2816 B |
Minimal setup
Use a Vue 3.5+ project with Vue SFC compilation configured (see Vite+ integration).
vp install @vizejs/composable
Import the entry directly. Call lifecycle-bound utilities inside <script setup> so cleanup follows the component scope; consult the runtime contract for this entry's SSR and hydration behavior.
import { onClickOutside } from "@vizejs/composable/on-click-outside";
Try it
Dismiss inline guide preferences
Keep an expandable preference region open during inside and ignored clicks, then dismiss it when an actual outside click occurs.
What to observe: Open the preferences and edit Review note. Apply local preference leaves the region open and reports the note. Keep preferences open is outside but ignored. Continue reading outside dismisses the region and increments the counter. Reopen it and press Escape inside: the example closes it, returns focus to its trigger, and leaves the outside-dismissal count unchanged.
The target is a template ref recreated by v-if; the trigger and separate keep-open button are ignored element targets. The composable tracks pointerdown as well as click, so a drag starting inside and ending outside does not dismiss it. Native keyboard activation of an outside button also emits a click. This is an inline non-modal region with ordinary Tab order; Escape and trigger focus are explicit example behavior, not features supplied by onClickOutside. The default window host is absent during SSR and scope disposal removes the client listeners. Applying a note changes only local display state.
Compare the initial and interacted states


Copy the example
Save this complete SFC in your Vue project. Its public imports and source are the same ones executed in the preview.
<script setup lang="ts">
import { ref, useId, useTemplateRef } from "vue";
import { onClickOutside } from "@vizejs/composable/on-click-outside";
const preferencesId = useId();
const panel = useTemplateRef<HTMLElement>("panel");
const trigger = useTemplateRef<HTMLButtonElement>("trigger");
const ignored = useTemplateRef<HTMLButtonElement>("ignored");
const open = ref(false);
const note = ref("Keep examples copyable.");
const dismissals = ref(0);
const message = ref("No dismissal yet.");
onClickOutside(
panel,
() => {
if (!open.value) return;
open.value = false;
dismissals.value += 1;
message.value = "Dismissed by an outside click.";
},
{ ignore: [trigger, ignored] },
);
function closeWithEscape(): void {
open.value = false;
message.value = "Closed with Escape inside the preferences.";
trigger.value?.focus();
}
</script>
<template>
<div class="composable-example">
<button
ref="trigger"
type="button"
:aria-expanded="open"
:aria-controls="`${preferencesId}-panel`"
@click="() => (open = !open)"
>
{{ open ? "Close guide preferences" : "Open guide preferences" }}
</button>
<section
v-if="open"
:id="`${preferencesId}-panel`"
ref="panel"
:aria-labelledby="`${preferencesId}-title`"
>
<h2 :id="`${preferencesId}-title`">Guide preferences</h2>
<label :for="`${preferencesId}-note`">Review note</label>
<input :id="`${preferencesId}-note`" v-model="note" @keydown.esc="closeWithEscape" />
<button
type="button"
@click="() => (message = `Local note: ${note}`)"
@keydown.esc="closeWithEscape"
>
Apply local preference
</button>
<p>
This inline region keeps ordinary Tab navigation. Escape closes it and focuses its trigger.
</p>
</section>
<div class="example-actions">
<button ref="ignored" type="button">Keep preferences open</button>
<button type="button">Continue reading outside</button>
</div>
<output aria-live="polite">Outside dismissals: {{ dismissals }} · {{ message }}</output>
<p>The trigger and Keep preferences open are ignored. Clicking inside preserves the region.</p>
</div>
</template>
Runtime contract
| Utility | Category | Stability | SSR | Hydration | Cleanup | Targets | Host globals | Uses |
|---|---|---|---|---|---|---|---|---|
onClickOutside |
events | experimental | deterministic-fallback | stable | caller, reactive-scope | web, desktop | globalThis, window |
resolveElement, tryOnScopeDispose |
API
onClickOutside
Call a handler when the user clicks (or focuses an iframe) outside an element. A click counts as outside only when its pointerdown also started outside the target, so drags that begin inside (e.g. text selection) and end outside are ignored. Event paths (composedPath()) make shadow-DOM targets work. The host is resolved once, when called. Nothing is registered during server rendering; listeners are removed with the owning scope or by the returned stop function.
function onClickOutside( target: MaybeElementTarget, handler: (event: Event) => void, options: OnClickOutsideOptions = {}, ): () => void
Types
ClickOutsideHost
Window-like capability observed by onClickOutside.
| Member | Type | Description |
|---|---|---|
document |
{ readonly activeElement: Element | null } |
Document used to detect focus moving into an iframe. |
OnClickOutsideOptions
Options for onClickOutside.
| Member | Type | Description |
|---|---|---|
ignore? |
MaybeRefOrGetter<readonly ClickOutsideIgnore[]> |
Elements (or selectors) whose clicks never count as outside, e.g. the button that toggles a popover. |
capture? |
boolean |
Listen in the capture phase so stopPropagation() inside the page cannot hide outside clicks. |
detectIframe? |
boolean |
Treat focus moving into an <iframe> outside the target as an outside click (iframes swallow pointer events). |
scheduler? |
TimeoutScheduler |
Owns the zero-delay timer used for iframe detection. |
host? |
MaybeRefOrGetter<ClickOutsideHost | null | undefined> |
Reactive window capability for alternate runtimes and tests. |