@svelte-put/clickoutside

GitHub Github

event for clicking outside node

@svelte-put/clickoutside @svelte-put/clickoutside @svelte-put/clickoutside @svelte-put/clickoutside @svelte-put/clickoutside

Still on Svelte 4? See the old docs site here.

Installation

npm install --save-dev @svelte-put/clickoutside
pnpm add -D @svelte-put/clickoutside
yarn add -D @svelte-put/clickoutside

New to Svelte 5? See Migration Guides.

Quick Start

<script lang="ts">
	import { clickoutside } from '@svelte-put/clickoutside';

	function doSomething(e: CustomEvent<MouseEvent>) {
		console.log(e.target);
	}
</script>

<div use:clickoutside onclickoutside={doSomething}>...</div>

Advanced Usage & Customization

Feature Demo

  Limit  

use:clickoutside is registered for this green box. Try these:

  1. Click within this green zone => won't trigger onclickoutside
  2. Click on the red zone => will trigger onclickoutside
  3. Click outside the red limit => won't trigger onclickoutside
  4. Enable/disable use:clickoutside with button below, then try (2) again

onclickoutside counter: 0

Expand the code block below to see how the demo is implemented.

<script lang="ts">
	import { clickoutside } from '@svelte-put/clickoutside';
	import { quintOut } from 'svelte/easing';
	import type { HTMLFieldsetAttributes } from 'svelte/elements';
	import { fly } from 'svelte/transition';

	let { class: cls, ...rest }: HTMLFieldsetAttributes = $props();

	let enabled = $state(true);
	let parent: Element | undefined = $state(undefined);
	let click = $state(0);

	function onClickOutside() {
		click++;
	}
	function toggleEnabled(e: Event) {
		e.stopPropagation();
		enabled = !enabled;
	}
</script>

<fieldset
	class="border-error-fg hl-error select-none border-4 p-10 {cls}"
	bind:this={parent}
	{...rest}
>
	<legend class="text-error-fg font-bold">&nbsp; Limit &nbsp;</legend>

	<div
		class="border-suscess-fg hl-success grid border-2 p-6"
		use:clickoutside={{ enabled, limit: { parent } }}
		onclickoutside={onClickOutside}
	>
		<p>
			<code>use:clickoutside</code> is registered for this <strong>green box</strong>. Try these:
		</p>
		<ol>
			<li>
				Click within this <strong>green</strong> zone => <strong>won't </strong> trigger
				<code>onclickoutside</code>
			</li>
			<li>
				Click on the <strong>red</strong> zone => <strong>will</strong> trigger
				<code>onclickoutside</code>
			</li>
			<li>
				Click outside the <strong>red</strong> limit => <strong>won't</strong> trigger
				<code>onclickoutside</code>
			</li>
			<li>Enable/disable <code>use:clickoutside</code> with button below, then try (2) again</li>
		</ol>
	</div>
	<div class="flex items-center justify-between">
		<p class="">
			<code>onclickoutside</code> counter:
			{#key click}
				<strong in:fly={{ y: 8, duration: 250, easing: quintOut }} class="inline-block"
					>{click}</strong
				>
			{/key}
		</p>
		<button class="c-btn" onclick={toggleEnabled}>
			{#if enabled}
				Disable
			{:else}
				Enable
			{/if}
		</button>
	</div>
</fieldset>

Limit the Clickoutside Zone

As seen in demo above, the limit.parent option can be set to limit the zone that will trigger clickoutside CustomEvent. By default, there is no limit, and the event listener is attached to document.

<script lang="ts">
  let parentNode: HTMLElement;
</script>

<div bind:this={parentNode}>
  <div use:clickoutside={{ limit: { parent: parentNode } }}>...</div>
</div>

Event Type Customization

By default, clickoutside is based on the click event. You can customize this with the event option.

<div use:clickoutside={{ event: 'mousedown' }}>...</div>

AddEventListenerOptions

Additional options can be passed to addEventListener should it be necessary.

<div use:clickoutside={{ options: { passive: true } }}>...</div>

 <!-- options => useCapture -->
<div use:clickoutside={{ options: true }}>...</div>

Excluding Other Events in the Clickoutside Zone

In the Advanced Usage & Customization feature demo, notice the stopPropagation is explicitly called in the event listener of the "Enable/Disable" button. Without this, the button would also trigger an clickoutside CustomEvent.

<button onclick={(e) => e.stopPropagation()}>...</button>

This makes sense since the button is technically outside of the element `clickoutside` is placed on, being within the "clickoutside zone".

Be aware to reflect customization made to the event listener as mentioned in the last two sections. For example, if the mousedown event is used instead of click, call stopPropagation on mousedown of the element that should not trigger clickoutside.

<div use:clickoutside={{ event: 'mousedown' }}>...</div>

<!-- later -->
<button onmousedown={(e) => e.stopPropagation()}>...</button>

Below is another demo where stopPropagation is generally recommended. Consider two buttons, each should trigger the corresponding box on its side:

  • The green toggle button behaves as expected. Notice the explicit stopPropagation call in the event listener.
  • The red toggle button does not behave as expected. Specifically:
    • when the left green box is toggled on, clicking on this red button causes the green box to toggled off, because the red button is indeed in outside the green box,
    • clicking on the red button does not seem to consistently toggle the red, for the same reason as before: the box does indeed get toggled on/off, but immediately off/on again because the red button triggers both the toggling and the clickoutside events.
Example
<script lang="ts">
	import { clickoutside } from '@svelte-put/clickoutside';

	let leftOpen = $state(true);
	function toggleLeft(e: Event) {
		e.stopPropagation();
		leftOpen = !leftOpen;
	}
	let rightOpen = $state(false);
	function toggleRight() {
		rightOpen = !rightOpen;
	}

	let containerEl: HTMLDivElement | undefined = $state(undefined);
</script>

<div class="relative w-full overflow-hidden" bind:this={containerEl}>
	<div
		class="bg-success-bg-100 absolute inset-y-0 left-0 grid w-1/3 origin-left place-items-center transition-[opacity_transform] {leftOpen
			? 'scale-x-100 opacity-100'
			: 'scale-x-50 opacity-50'}"
		use:clickoutside={{ enabled: leftOpen, limit: { parent: containerEl } }}
		onclickoutside={toggleLeft}
	>
		<div class="i i-[arrow-right] h-8 w-8"></div>
	</div>
	<div class="mx-auto flex w-1/3">
		<button
			onclick={toggleLeft}
			class="hl-success inline flex-1 cursor-pointer p-2 active:scale-95"
		>
			Toggle Left
		</button>
		<button onclick={toggleRight} class="hl-error inline flex-1 cursor-pointer p-2 active:scale-95">
			Toggle Right
		</button>
	</div>
	<div
		class="bg-error-bg-100 absolute inset-y-0 right-0 grid w-1/3 origin-right place-items-center {rightOpen
			? 'scale-x-100 opacity-100'
			: 'scale-x-50 opacity-50'}"
		use:clickoutside={{ enabled: rightOpen, limit: { parent: containerEl } }}
		onclickoutside={toggleRight}
	>
		<div class="i i-[arrow-left] h-8 w-8"></div>
	</div>
</div>

Migration Guides

V3 -> V4 (Svelte 5 in Runes mode)

When migrating to V4, you will need to change event directive on:clickoutside to standard attribute onclickoutside (remove :).

<div use:clickoutside on:clickoutside></div>
<div use:clickoutside onclickoutside></div>

Additionally, event modifier |stopPropagation is no longer support in Svelte, either use onclickcapture or explicitly call e.stopPropagation() in your event handler.


Happy clicking outside!

Edit this page on Github