useScrollPosition
Track the scroll position of an element ref, a raw HTMLElement, window or document. Returns two reactive refs — x and y — that update on every scroll event and re-read when the target changes.
Demo
y: 0pxx: 0px0%
Scroll this box ↓
Line 1
Line 2
Line 3
Line 4
Line 5
Line 6
Line 7
Line 8
Line 9
Line 10
Line 11
Line 12
You reached the bottom 🎉
<script setup lang="ts">
import { computed, useTemplateRef } from 'vue';
import { useScrollPosition } from '@basmilius/common';
const scroller = useTemplateRef<HTMLDivElement>('scroller');
const {x, y} = useScrollPosition(scroller);
const progress = computed(() => {
const element = scroller.value;
if (!element) {
return 0;
}
const max = element.scrollHeight - element.clientHeight;
return max > 0 ? Math.round((y.value / max) * 100) : 0;
});
</script>
<template>
<div class="demo">
<div class="readout">
<span>y: <b>{{ y }}</b>px</span>
<span>x: <b>{{ x }}</b>px</span>
<span><b>{{ progress }}</b>%</span>
</div>
<div class="track">
<div class="fill" :style="{width: `${progress}%`}"/>
</div>
<div ref="scroller" class="scroller">
<div class="content">
<p>Scroll this box ↓</p>
<p v-for="line in 12" :key="line">Line {{ line }}</p>
<p>You reached the bottom 🎉</p>
</div>
</div>
</div>
</template>
<style scoped>
.demo {
padding: 24px;
border: 1px solid var(--vp-c-border);
border-radius: 12px;
background: var(--vp-c-bg-soft);
}
.readout {
display: flex;
gap: 20px;
margin-bottom: 12px;
font-family: var(--vp-font-family-mono);
font-size: 14px;
color: var(--vp-c-text-2);
}
.readout b {
color: var(--vp-c-brand-1);
}
.track {
height: 6px;
margin-bottom: 16px;
border-radius: 999px;
overflow: hidden;
background: var(--vp-c-bg);
}
.fill {
height: 100%;
border-radius: 999px;
background: var(--vp-c-brand-1);
transition: width .05s linear;
}
.scroller {
height: 180px;
overflow: auto;
border-radius: 8px;
background: var(--vp-c-bg);
}
.content {
padding: 16px;
}
.content p {
margin: 0 0 24px;
color: var(--vp-c-text-2);
}
.content p:first-child {
font-weight: 600;
color: var(--vp-c-text-1);
}
</style>Importing
ts
import { useScrollPosition } from '@basmilius/common';Usage
vue
<script setup lang="ts">
import { useScrollPosition } from '@basmilius/common';
const {x, y} = useScrollPosition(window);
</script>
<template>
<div class="indicator">Scrolled to {{ x }} × {{ y }}</div>
</template>Pass a template ref to track a scrollable container instead of the viewport:
vue
<script setup lang="ts">
import { useTemplateRef } from 'vue';
import { useScrollPosition } from '@basmilius/common';
const scroller = useTemplateRef<HTMLDivElement>('scroller');
const {y} = useScrollPosition(scroller);
</script>
<template>
<div ref="scroller" class="scroller">
<p :class="{shadow: y > 0}">…</p>
</div>
</template>For window the position comes from scrollX / scrollY, for document from documentElement.scrollLeft / scrollTop, and for elements from scrollLeft / scrollTop. The scroll listener is registered through useEventListener with { passive: true }; element and component refs are unwrapped via unwrapTarget.
Type signature
ts
type EligibleTarget = HTMLElement | ComponentPublicInstance | Window | Document;
declare function useScrollPosition(
target: MaybeRefOrGetter<EligibleTarget | null | undefined>
): {
readonly x: Ref<number>;
readonly y: Ref<number>;
};