Trigger functions and events based on an element's position on screen — a tiny, dependency-free Vue 3 wrapper around the native
IntersectionObserver.
Simple demo page — open your browser console and watch the events fire while scrolling up and down.
- Vue 3, written in TypeScript with shipped type declarations
- Zero runtime dependencies — a thin wrapper over the native
IntersectionObserver - ESM + CJS builds (tree-shakable) with correct
exports - Flexible: custom tag, custom observer options, reactive
activetoggle - Optional CSS helper classes for quick, configuration-free animations
- Slot support exposing the live waypoint state
useWaypointcomposable for observing any template ref directly- SSR-safe
npm i vue-waypoint<script setup lang="ts">
import { Waypoint, type WaypointState } from "vue-waypoint";
function onChange(state: WaypointState) {
// state.going -> "IN" | "OUT"
// state.direction -> "UP" | "DOWN" | "LEFT" | "RIGHT"
// state.el -> the observed Element
console.log(state.going, state.direction);
}
</script>
<template>
<Waypoint @change="onChange">
<!-- anything you want here -->
</Waypoint>
</template><script lang="ts">
import { defineComponent } from "vue";
import { Waypoint, type WaypointState } from "vue-waypoint";
export default defineComponent({
components: { Waypoint },
setup() {
const onChange = (state: WaypointState) => {
console.log(state.going, state.direction);
};
return { onChange };
},
});
</script>
<template>
<Waypoint @change="onChange" />
</template>Reactively enable or disable the waypoint. The element is observed while active is true and unobserved when it flips to false.
- Enable:
<Waypoint :active="true" /> - Disable:
<Waypoint :active="false" />
A standard IntersectionObserverInit object, forwarded verbatim to the underlying observer.
const options: IntersectionObserverInit = {
root: document,
rootMargin: "0px 0px 0px 0px",
threshold: [0.25, 0.75],
};<Waypoint :options="options" />
The rendered element tag. Defaults to div.
<Waypoint tag="div" />→<div class="waypoint"></div><Waypoint tag="span" />→<span class="waypoint"></span><Waypoint tag="p" />→<p class="waypoint"></p>
Disable the automatic CSS helper classes. Defaults to false.
- With helpers (default):
<Waypoint />→<div class="waypoint going-in direction-down"></div> - Without helpers:
<Waypoint :disable-css-helpers="true" />→<div></div>
Zero configuration, handy for simple CSS animations. The component toggles three families of classes:
waypoint— set as soon as the waypoint is readygoing-in/going-out— toggled as the element enters and leaves the viewportdirection-up/direction-down/direction-left/direction-right— toggled as the scroll direction changes
Examples:
waypoint going-in direction-up— visible, came from the bottom, scrolling up (natural scroll)waypoint going-in direction-down— visible, came from the top, scrolling downwaypoint going-out direction-up— hidden, was scrolling upwaypoint going-out direction-down— hidden, was scrolling down
Emitted every time the waypoint detects an intersection change.
interface WaypointState {
el: Element | undefined;
going: "IN" | "OUT" | undefined;
direction: "UP" | "DOWN" | "LEFT" | "RIGHT" | undefined;
}<template>
<Waypoint @change="onChange" />
</template>The same WaypointState is also exposed through the default slot:
<Waypoint #default="{ going, direction }">
<span v-if="going">going-{{ going.toLowerCase() }}</span>
<span v-if="direction">direction-{{ direction.toLowerCase() }}</span>
</Waypoint>Prefer composing over a component? useWaypoint observes a template ref directly, with the same reactive active/options behavior as <Waypoint>:
<script setup lang="ts">
import { ref } from "vue";
import { useWaypoint } from "vue-waypoint";
const target = ref<Element | null>(null);
const { state } = useWaypoint(target, {
active: true,
observerOptions: { threshold: 0.5 },
});
</script>
<template>
<div ref="target">{{ state?.going }}</div>
</template>active and observerOptions accept a plain value or a Ref. The observer is created lazily (only once the target element and active are both truthy) and is disconnected automatically when the enclosing component scope is torn down. Outside a component scope, call the returned stop() yourself.
npm i # install
npm run dev # run the demo app
npm run lint # eslint (flat config)
npm run type-check
npm test # vitest
npm run build # type-check + library build (ESM + CJS + .d.ts)The Vue 2 line lives on the vue2 branch. The 4.x/5.x releases target Vue 3 only.