Visibility Tracking
useVisibilityTracker() — per-key "entered viewport" / "left viewport" tracking, backed by a real IntersectionObserver rather than diffing visibleRange — so it's accurate even with a large overscan buffer or partial-visibility thresholds. Dataset-agnostic: you decide which elements to observe(), each under whatever key you like (a row id, an index, anything).
Typical use: highlight a nav/minimap entry, a table-of-contents item, or a "jump to" button in a control panel while the corresponding row is actually on screen in a virtualized list or table — turning it off the instant the row scrolls back out.
function useVisibilityTracker(options: UseVisibilityTrackerOptions): UseVisibilityTrackerReturnOptions
root
() => HTMLElement | null
Returns the scroll container to intersect against. Omit to use the browser viewport.
rootMargin
string · default: '0px'
IntersectionObserver rootMargin — grow/shrink the root's effective bounds (e.g. trigger slightly early).
threshold
number | number[] · default: 0
Fraction of the element that must be visible to count as "visible".
onVisible
(key: string | number) => void
Called when a tracked key becomes visible.
onHidden
(key: string | number) => void
Called when a tracked key becomes hidden (including via unobserve while it was visible).
Return value
visibleKeys
Readonly<Ref<Set<string | number>>>
Keys currently intersecting the root.
isVisible
(key: string | number) => boolean
Whether key is currently visible.
observe
(el: Element | null, key: string | number) => void
Start tracking an element under key — bind via a template ref callback.
unobserve
(key: string | number) => void
Stop tracking key (e.g. on row unmount).
rootmay resolve after this composable's own setup runs (e.g. a siblingVirtualList's template ref) — it's polled for a few frames, and rebuilt automatically if the resolved root element ever changes (such as after a:key-forced remount).
Example
Watch specific rows and mirror their visibility into a sidebar panel — same pattern used by the VirtualList demo's "Watchlist":
<script setup lang="ts">
import { ref } from 'vue'
import { VirtualList, useVisibilityTracker } from 'vue-virtual-scroller-kit'
import type { VirtualListExpose } from 'vue-virtual-scroller-kit'
interface Row {
id: number
title: string
}
const items = ref<Row[]>(
Array.from({ length: 100_000 }, (_, i) => ({ id: i + 1, title: `Row ${i + 1}` })),
)
const listRef = ref<VirtualListExpose | null>(null)
const watchedIds = ref<Set<number>>(new Set([1, 50_000]))
const tracker = useVisibilityTracker({
root: () => listRef.value?.getScrollElement() ?? null,
})
// Rows unmount when scrolled out of the virtualized range, so track/untrack on
// mount/unmount rather than assuming an observed element stays alive.
function onRowMount(el: Element, id: number) {
if (watchedIds.value.has(id)) tracker.observe(el, id)
}
function onRowUnmount(id: number) {
tracker.unobserve(id)
}
</script>
<template>
<VirtualList
ref="listRef"
:items="items"
key-field="id"
:estimated-item-size="48"
style="height: 500px"
>
<template #default="{ item }">
<div
:ref="(el) => el && onRowMount(el as Element, item.id)"
@vue:unmounted="onRowUnmount(item.id)"
:style="{
background:
watchedIds.has(item.id) && tracker.isVisible(item.id) ? '#fef08a' : 'transparent',
}"
>
{{ item.title }}
</div>
</template>
</VirtualList>
</template>