Skip to content

useIntersection

Adds 2 new behaviors to your Stimulus controller: appear and disappear.

This behavior is built on top of the Intersection Observer API

Reference

javascript
useIntersection(controller, options = {})

controller : a Stimulus Controller (usually 'this')

options : a single object that accepts both the stimulus-use specific options listed below and any IntersectionObserver option. stimulus-use reads the options it knows about and forwards the whole object to the underlying IntersectionObserver, so there is no separate place to configure the observer.

stimulus-use options

These options control how stimulus-use reports intersections. They are consumed by stimulus-use and ignored by the browser.

OptionDescriptionDefault value
dispatchEventWhether to dispatch appear, disappear events or not.true
elementThe root element listening to intersection events.The controller element
eventPrefixWhether to prefix or not the emitted event. Can be a boolean or a string.
- true prefix the event with the controller identifier card:appear
- someString prefix the event with the given string someString:appear
- false to remove prefix
true
visibleAttributeThe name of the attribute which gets added to the tracked element when the element is visibleisVisible

IntersectionObserver options

The same options object is passed straight to the IntersectionObserver constructor. This is how you tune when the appear and disappear callbacks fire — for example, raising the threshold so they only run once a portion of the element is visible, or changing the root to observe inside a scroll container instead of the viewport. You don't need to create or access the observer yourself.

The descriptions below are from MDN. If options isn't specified, the observer uses the document's viewport as the root, with no margin, and a 0% threshold (meaning that even a one-pixel change is enough to trigger a callback). You can provide any combination of the following options:

OptionDescriptionDefault value
rootAn Element or Document object which is an ancestor of the intended target, whose bounding rectangle will be considered the viewport. Any part of the target not visible in the visible area of the root is not considered visible.document viewport
rootMarginA string which specifies a set of offsets to add to the root's bounding_box when calculating intersections, effectively shrinking or growing the root for calculation purposes. The syntax is approximately the same as that for the CSS margin property; see The intersection root and root margin for more information on how the margin works and the syntax."0px 0px 0px 0px"
thresholdEither a single number or an array of numbers between 0.0 and 1.0, specifying a ratio of intersection area to total bounding box area for the observed target. A value of 0.0 means that even a single visible pixel counts as the target being visible. 1.0 means that the entire target element is visible. See Thresholds for a more in-depth description of how thresholds are used. 0.0

Usage

Composing

js
import { Controller } from '@hotwired/stimulus'
import { useIntersection } from 'stimulus-use'

export default class extends Controller {
  connect() {
    useIntersection(this)
  }

  appear(entry, observer) {
    // callback automatically triggered when the element
    // intersects with the viewport (or root Element specified in the options)
  }

  disappear(entry, observer) {
    // callback automatically triggered when the element
    // leaves the viewport (or root Element specified in the options)
  }
}

Passing IntersectionObserver options

Because options is forwarded to the IntersectionObserver, you can configure intersection behavior right where you call useIntersection, without ever touching the observer directly. A common case is delaying appear/disappear until a portion of the element is on screen via threshold:

js
import { Controller } from '@hotwired/stimulus'
import { useIntersection } from 'stimulus-use'

export default class extends Controller {
  connect() {
    useIntersection(this, {
      // only trigger `appear` once 80% of the element is visible
      threshold: 0.8,

      // grow or shrink the root box before intersections are computed
      rootMargin: '0px 0px -200px 0px',

      // observe relative to a scroll container instead of the viewport
      root: document.querySelector('#scroll-container')
    })
  }

  appear(entry, observer) {
    // triggered once the threshold / rootMargin conditions are met
  }

  disappear(entry, observer) {
    // ...
  }
}

Extending a controller

js
import { IntersectionController } from 'stimulus-use'

export default class extends IntersectionController {
  options = {
    element: this.element, // default
  }

  appear(entry, observer) {
    // ...
  }

  disappear(entry, observer) {
    // ...
  }
}

Events

This module adds two new events appear and disapear event that you may use to triggers Stimulus Actions.

For example, to count all visible elements on a page we could listen to individual appear/disappear events to update a counter

js
import { Controller } from '@hotwired/stimulus'
import { useIntersection } from 'stimulus-use'

export default class extends Controller {
  connect() {
    useIntersection(this, { eventPrefix: false })
  }

  increase() { /* ... */ }
  decrease() { /* ... */ }
}
html
<div
  class="modal"
  data-controller="counter"
  data-action="appear@window->counter#increase disappear@window->counter#decrease"
>
</div>

Since the data-controller and the data-action are on the same element you can even leave out the @window because you don't need to wait for the event to bubble up the DOM tree to the window. The event gets dispatched on the controller element (if not overridden by the element option).

html
<div
  class="modal"
  data-controller="counter"
  data-action="appear->counter#increase disappear->counter#decrease"
>
</div>

Event Details

Get the emitting controller and entry object for an appear event

js
count(event) {
  const { controller, entry, observer } = event.detail
}

Helper functions

If you are tracking multiple events in your controller you might find these helper functions handy:

HelperDescription
this.isVisible() / this.allVisible()Returns true if all of the tracked elements are visible.
this.noneVisible()Returns true if none of the tracked elements are visible.
this.oneVisible()Returns true if exactly one of the tracked elements is visible.
this.atLeastOneVisible()Returns true if at least one of the tracked elements is visible.

Using Helper functions

js
import { Controller } from '@hotwired/stimulus'
import { useIntersection } from 'stimulus-use'

export default class extends Controller {
  static targets = [ 'menu' ]

  connect() {
    useIntersection(this)
  }

  appear() {
    if (this.atLeastOneVisible()) {
      this.menuTarget.show()
    }
  }

  disappear() {
    if (this.noneVisible()) {
      this.menuTarget.hide()
    }
  }
}

Manually calling observe() and unobserve()

You can manually call observe() and unobserve() by obtaining references from the useIntersection() function call.

useIntersection() returns an array with two functions, the first one is the observe() and the second one is the unobserve() function.

js
export default class extends Controller {
  connect() {
    const [observe, unobserve] = useIntersection(this)
    this.observe = observe
    this.unobserve = unobserve
  }

  appear() {
    // observe and emit `appear()` callback just once
    this.unobserve()
  }
}

Accessing the IntersectionObserver instance

You have the freedom to perform more advanced operations on the observer directly, so you can customize the logic according to your needs. The IntersectionObserver instance gets passed in as the second argument to the appear and disappear callbacks.

js
export default class extends Controller {
  connect() {
    useIntersection(this)
  }

  appear(entry, observer) {
    // observe and emit `appear()` callback just once
    observer.unobserve(entry.target)
  }
}

Alternatively, you can also access the observer from the event detail.

Example

Rails Infinite scroll: https://github.com/adrienpoly/infinite-scroll-stimulus-js