---
title: useIntersection composable
related:
  - title: Intersection
    path: ../vue-components/intersection.md
  - title: v-intersection directive
    path: ../vue-directives/intersection.md
---
The `useIntersection()` composable tells you whether an element is visible on screen (or inside a scrolling parent), through a reactive Boolean. Under the hood it uses the [Intersection Observer API](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API).

It is the setup-code counterpart of the [Intersection directive](../vue-directives/intersection.md) and of the [QIntersection](../vue-components/intersection.md) component: all three share one Intersection Observer per configuration, so observing many elements stays cheap to scroll. Use the composable when you want the state on your component or on any element or component ref, without wrapping it in an extra DOM element.

> [!NOTE]
> On the server-side of SSR or SSG modes, the composable never observes anything: `isIntersecting` stays `false` until the client takes over.

> [!TIP]
> **Outside of a component**
>
> The composable can also be called outside of `setup()`: in a boot file, a store or a plain module. There is no component root to fall back on and no mount to wait for, so supply a `target` (an element, or a ref or getter of one); the observation starts right away and nothing stops it by itself: call `stopIntersection()` when you are done.

## Syntax

```js
import { useTemplateRef } from 'vue'
import { useIntersection } from 'quasar'

setup () {
  const target = useTemplateRef('target') // an Element or a component

  const { isIntersecting, refreshIntersection, stopIntersection } = useIntersection({
    // all optional:
    target,               // (default: the component's own root element)
    root: null,           // Element used as viewport (default: null, the browser viewport)
    rootMargin: '10px',   // CSS-like margin around the root (default: '0px')
    threshold: 0.5,       // Number or Array of Numbers, 0 to 1 (default: 0)
    once: true,           // stop observing after the first time the target is visible (default: false)
    disabled: true,       // pause observing (default: false)
    onIntersect (entry) { // called with every IntersectionObserverEntry
      // return false to stop observing
    }
  })

  // ...
}
```

```ts
function useIntersection(
  options?: MaybeRefOrGetter<{
    target?: MaybeRefOrGetter<
      Element | ComponentPublicInstance | null | undefined
    >
    root?: Element | Document | null
    rootMargin?: string
    threshold?: number | number[]
    once?: boolean
    disabled?: boolean
    onIntersect?: (entry: IntersectionObserverEntry) => false | void
  }>
): {
  isIntersecting: Ref<boolean>
  refreshIntersection: () => void
  stopIntersection: () => void
}
```

Without a `target`, the composable observes the root element of the component it is called in, as of the moment the component gets mounted. A component rendering a fragment (multiple root nodes) has no root element to observe, so supply a `target` there.

`refreshIntersection()` makes the observer report the current state again, whether it changed or not (it does nothing while `disabled` or before the target exists). An observer only reports changes on its own, so this is for the cases where your code needs a fresh verdict after doing something to the layout, such as knowing whether a target that was in view still is after the content around it grew.

`stopIntersection()` ends the observation for good. You will rarely need it, as the composable stops by itself when the component gets destroyed.

## Changing the options while running

The options can be a plain Object, a Ref or a getter Function. A plain Object is read once. With a Ref or a getter, the composable tracks whatever reactive state the options read and re-applies them whenever that state changes, so you never call anything to "update" it:

- toggling `disabled` pauses and resumes the observation (`isIntersecting` keeps its last value while paused)
- changing `root`, `rootMargin` or `threshold` moves the target to an observer with the new configuration
- pointing `target` to another element (or letting a template ref change through `v-if`) follows it
- swapping `onIntersect` takes effect from the next entry

Options that resolve to the same values are a no-op, so a getter that rebuilds the Object on every run costs nothing extra.

```js
import { ref } from 'vue'
import { useIntersection } from 'quasar'

setup () {
  const paused = ref(false)
  const threshold = ref(0)

  const { isIntersecting } = useIntersection(() => ({
    disabled: paused.value,
    threshold: threshold.value
  }))

  function pause () { paused.value = true }
  function resume () { paused.value = false }

  // from now on the handler fires only when at least
  // half of the element is visible
  function requireHalf () { threshold.value = 0.5 }

  // ...
}
```

The same works with a Ref holding the whole options Object:

```js
const options = ref({ rootMargin: '0px' })

const { isIntersecting } = useIntersection(options)

// later on
options.value = { rootMargin: '200px' }
```

A `once` observation that has already fired stays off for as long as `once` holds: toggling `disabled` or changing any other option does not start observing again. Setting `once` back to `false` does, and the observation can retire again the next time `once` is turned on. An `onIntersect` hook that returned `false`, or a call to `stopIntersection()`, ends the observation for good.

## Example

The card below is observed through a template ref, with the scrolling box as `root` and a list of thresholds so that the `onIntersect` hook reports the visible ratio. Toggle `once` to stop observing after the next time the card is in view, and toggle it back off to observe again:

Example "Observing an element":

```vue
<template>
  <div class="q-gutter-sm row items-center">
    <q-badge
      :color="isIntersecting ? 'positive' : 'negative'"
      :label="isIntersecting ? 'in view' : 'out of view'"
    />
    <q-badge color="secondary" :label="`ratio: ${ratio}`" />
    <q-toggle v-model="once" label="once" />
  </div>

  <div ref="listRef" class="scroll list rounded-borders q-mt-md">
    <div class="filler">Scroll down...</div>

    <q-card ref="cardRef" flat bordered class="q-mx-auto q-my-md card">
      <img alt="Mountains" src="https://cdn.quasar.dev/img/mountains.jpg" />
      <q-card-section>
        <div class="text-h6">The observed card</div>
      </q-card-section>
    </q-card>

    <div class="filler">...and back up</div>
  </div>
</template>

<script setup>
import { ref, useTemplateRef } from 'vue'
import { useIntersection } from 'quasar'

const listRef = useTemplateRef('listRef')
const cardRef = useTemplateRef('cardRef')

const once = ref(false)
const ratio = ref(0)

const { isIntersecting } = useIntersection(() => ({
  target: cardRef,
  root: listRef.value,
  threshold: [0, 0.25, 0.5, 0.75, 1],
  once: once.value,
  onIntersect(entry) {
    ratio.value = Math.round(entry.intersectionRatio * 100) / 100
  }
}))
</script>

<style lang="sass" scoped>
.list
  height: 250px
  border: 1px solid #fff
  outline: 1px solid #000

.card
  max-width: 300px

.filler
  height: 400px
  display: flex
  align-items: center
  justify-content: center
</style>
```

To observe the component's own root element instead, omit the `target`. The card below starts loading its image a bit before it enters the viewport and, thanks to `once`, is not observed anymore after that:

```html
<template>
  <div class="card">
    <img v-if="isIntersecting" :src="src" />
  </div>
</template>

<script setup>
  import { useIntersection } from 'quasar'

  defineProps({ src: String })

  // observes this component's root element (the card)
  const { isIntersecting } = useIntersection({
    rootMargin: '200px',
    once: true
  })
</script>
```
