Skip to content

Element Visibility ​

useElementVisibility(target, options?) — reactive IntersectionObserver-backed visibility of an element. SSR-safe: observation starts once target resolves to a real element on the client.

ts
function useElementVisibility(
  target: MaybeRefOrGetter<HTMLElement | null | undefined>,
  options?: UseElementVisibilityOptions,
): UseElementVisibilityReturn

Every call shares one module-level createVisibilityEngine() instance — consistent with the engine's own pooled-observer design.

Options ​

threshold ​

MaybeRefOrGetter<number | number[] | undefined> · default: viewportDefaults.threshold (0 unless overridden)

Changing this — as a ref/getter — re-subscribes for real: pool membership is keyed on it, the same as rootMargin/root.

rootMargin ​

MaybeRefOrGetter<string | undefined> · default: viewportDefaults.rootMargin ('0px' unless overridden)

Same re-subscribe behavior as threshold.

root ​

MaybeRefOrGetter<HTMLElement | null | undefined> · default: null

Same re-subscribe behavior as threshold/rootMargin.

once ​

MaybeRefOrGetter<boolean | undefined> · default: viewportDefaults.once (false unless overridden)

Unlike threshold/rootMargin/root, changing this updates the existing subscription live — no re-subscribe, no observer churn.

onEnter ​

(info: IntersectionInfo) => void · default: —

onLeave ​

(info: IntersectionInfo) => void · default: —

onEnter/onLeave are deliberately plain values, not MaybeRefOrGetter — a callback closing over a ref's .value already reads it fresh on every call, since Vue composables run once in setup(). There's no staleness for a wrapper to fix here.

threshold/rootMargin/once fall back to the package-wide viewportDefaults object rather than a hardcoded 0/'0px'/false — see setViewportDefaults() below, and Nuxt Module for how it's typically set at the app level.

Return value ​

isVisible ​

Ref<boolean>

ratio ​

Ref<number>

The element's current intersectionRatio.

Example:

vue
<script setup lang="ts">
import { ref } from 'vue'
import { useElementVisibility } from '@macrulez/inview-vue'

const card = ref<HTMLElement | null>(null)
const { isVisible } = useElementVisibility(card, { threshold: 0.5, once: true })
</script>

<template>
  <div ref="card" :class="{ 'is-visible': isVisible }">...</div>
</template>

Re-subscribes automatically whenever target, threshold, rootMargin, or root change (as a ref/getter); once updates the existing subscription live instead.

threshold/rootMargin/root/once all accept a ref/getter, not just a plain value:

vue
<script setup lang="ts">
const stopOnceUserOptsIn = ref(false)
const { isVisible } = useElementVisibility(card, { once: stopOnceUserOptsIn })
// later: stopOnceUserOptsIn.value = true — updates live, no re-subscribe
</script>

Global Defaults ​

setViewportDefaults() / viewportDefaults — package-wide defaults for threshold/rootMargin/once, overridable once at the app level instead of passing the same options to every call site. Read by this composable, and by v-reveal and <InView> too.

ts
interface ViewportDefaults {
  threshold: number | number[]
  rootMargin: string
  once: boolean
}

const viewportDefaults: ViewportDefaults // { threshold: 0, rootMargin: '0px', once: false }

function setViewportDefaults(overrides: Partial<ViewportDefaults>): void

setViewportDefaults() only overwrites the fields you actually pass — it merges into the shared viewportDefaults object rather than replacing it. This is exactly what @macrulez/inview-nuxt calls under the hood from its module options.