useEventListener
Attach an event listener to an element ref, a raw HTMLElement, window or document. The listener is bound when the target resolves, detached when the target changes or the component scope is disposed, and re-attached automatically when the target points at something new. A stop function is returned to detach manually.
Demo
Move, hover and click anywhere inside this box.
- Pointer
- 0 × 0
- Inside
- no
- Clicks
- 0
<script setup lang="ts">
import { ref, useTemplateRef } from 'vue';
import { useEventListener } from '@basmilius/common';
const area = useTemplateRef<HTMLDivElement>('area');
const position = ref({x: 0, y: 0});
const inside = ref(false);
const clicks = ref(0);
useEventListener(area, 'pointermove', evt => {
const rect = (evt.currentTarget as HTMLElement).getBoundingClientRect();
position.value = {
x: Math.round(evt.clientX - rect.left),
y: Math.round(evt.clientY - rect.top)
};
});
useEventListener(area, ['pointerenter', 'pointerleave'], evt => {
inside.value = evt.type === 'pointerenter';
});
useEventListener(area, 'click', () => {
clicks.value++;
});
</script>
<template>
<div ref="area" class="demo" :class="{active: inside}">
<p class="hint">Move, hover and click anywhere inside this box.</p>
<dl class="readout">
<div>
<dt>Pointer</dt>
<dd>{{ position.x }} × {{ position.y }}</dd>
</div>
<div>
<dt>Inside</dt>
<dd>{{ inside ? 'yes' : 'no' }}</dd>
</div>
<div>
<dt>Clicks</dt>
<dd>{{ clicks }}</dd>
</div>
</dl>
</div>
</template>
<style scoped>
.demo {
padding: 24px;
border: 1px solid var(--vp-c-border);
border-radius: 12px;
background: var(--vp-c-bg-soft);
cursor: crosshair;
transition: border-color .2s, background .2s;
user-select: none;
}
.demo.active {
border-color: var(--vp-c-brand-1);
}
.hint {
margin: 0 0 16px;
color: var(--vp-c-text-2);
font-size: 14px;
}
.readout {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
margin: 0;
}
.readout div {
padding: 12px;
border-radius: 8px;
background: var(--vp-c-bg);
text-align: center;
}
.readout dt {
margin-bottom: 4px;
color: var(--vp-c-text-3);
font-size: 12px;
text-transform: uppercase;
letter-spacing: .04em;
}
.readout dd {
margin: 0;
font-family: var(--vp-font-family-mono);
font-size: 18px;
font-weight: 600;
color: var(--vp-c-brand-1);
}
</style>Global targets
window and document are valid targets too. Because reading window.innerWidth during component setup touches a browser global, the demo component is wrapped in VitePress' built-in <ClientOnly> so it only renders in the browser:
<script setup lang="ts">
import { ref } from 'vue';
import { useEventListener } from '@basmilius/common';
const width = ref(window.innerWidth);
const height = ref(window.innerHeight);
const lastKey = ref('—');
useEventListener(window, 'resize', () => {
width.value = window.innerWidth;
height.value = window.innerHeight;
});
useEventListener(document, 'keydown', evt => {
lastKey.value = evt.key;
});
</script>Importing
import { useEventListener } from '@basmilius/common';Usage
<script setup lang="ts">
import { ref, useTemplateRef } from 'vue';
import { useEventListener } from '@basmilius/common';
const button = useTemplateRef<HTMLButtonElement>('button');
const clicks = ref(0);
useEventListener(button, 'click', () => {
clicks.value++;
});
useEventListener(window, ['resize', 'orientationchange'], () => {
console.log('viewport changed');
});
</script>
<template>
<button ref="button">Clicked {{ clicks }} times</button>
</template>The target may be a template ref, a raw HTMLElement, window or document. Element and component refs are unwrapped via unwrapTarget, so passing a component instance works as well.
Pass a single event name or an array of names. The third argument is the listener; the event object is typed against the combined DOM event maps, so evt is inferred from the event name. The optional fourth argument accepts the standard addEventListener options ({ passive: true }, { capture: true }, …).
const stop = useEventListener(document, 'keydown', evt => {
if (evt.key === 'Escape') {
stop();
}
});Cleanup happens automatically on scope dispose; call the returned stop only when you want to detach earlier.
Type signature
type EligibleTarget = HTMLElement | ComponentPublicInstance | Window | Document;
type EventMap = HTMLElementEventMap & WindowEventMap & DocumentEventMap;
declare function useEventListener<TType extends keyof EventMap>(
target: MaybeRefOrGetter<EligibleTarget | null | undefined>,
type: TType | TType[],
listener: (evt: EventMap[TType]) => void,
options?: boolean | AddEventListenerOptions
): () => void;