Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/htmx.js
Original file line number Diff line number Diff line change
Expand Up @@ -814,14 +814,14 @@ var htmx = (() => {
spec.observer = new IntersectionObserver((entries) => {
for (let i = 0; i < entries.length; i++) {
if (entries[i].isIntersecting) {
this.trigger(elt, 'intersect', {}, false);
this.trigger(entries[i].target, 'intersect', {}, false);
if (isRevealed) spec.observer.disconnect();
break;
}
}
}, observerOptions);
eventName = 'intersect';
spec.observer.observe(elt);
for (let fromElt of fromElts) spec.observer.observe(fromElt);
}

// Every: set up interval
Expand Down
4 changes: 2 additions & 2 deletions test/tests/attributes/hx-on.js
Original file line number Diff line number Diff line change
Expand Up @@ -583,7 +583,7 @@ describe('hx-on="eventSpec -> code" syntax', function() {
};
let div = createProcessedHTML('<div hx-on="revealed -> this.classList.add(\'visible\')">x</div>');
div.classList.contains('visible').should.equal(false);
observerCallback([{ isIntersecting: true }]);
observerCallback([{ isIntersecting: true, target: div }]);
div.classList.contains('visible').should.equal(true);
window.IntersectionObserver = originalIO;
});
Expand All @@ -597,7 +597,7 @@ describe('hx-on="eventSpec -> code" syntax', function() {
};
let div = createProcessedHTML('<div hx-on="intersect once -> this.classList.add(\'in-view\')">x</div>');
div.classList.contains('in-view').should.equal(false);
observerCallback([{ isIntersecting: true }]);
observerCallback([{ isIntersecting: true, target: div }]);
div.classList.contains('in-view').should.equal(true);
window.IntersectionObserver = originalIO;
});
Expand Down
24 changes: 24 additions & 0 deletions test/tests/unit/__initializeTriggers.js
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,30 @@ describe('__initializeTriggers unit tests', function() {
assert.isUndefined(captured[0].root);
assert.isUndefined(captured[0].threshold);
});

it('from: modifier observes the from element, not the trigger element', function() {
let observed = [];
const origIO = window.IntersectionObserver;
window.IntersectionObserver = function(cb, opts) {
const io = new origIO(cb, opts);
const origObserve = io.observe.bind(io);
io.observe = (el) => { observed.push(el); origObserve(el); };
return io;
};
playground().innerHTML = '<div id="scroller3"><div id="sentinel" hx-action="js:null" hx-trigger="intersect from:#scroller3">Test</div></div>';
htmx.process(playground());
window.IntersectionObserver = originalIO;
assert.equal(observed.length, 1);
assert.equal(observed[0], document.getElementById('scroller3'));
assert.notEqual(observed[0], document.getElementById('sentinel'));
});

it('from: modifier with threshold passes both to IntersectionObserver', function() {
playground().innerHTML = '<div id="scroller4"><div id="target4" hx-action="js:null" hx-trigger="intersect from:#scroller4 threshold:0.75">Test</div></div>';
htmx.process(playground());
assert.equal(captured.length, 1);
assert.equal(captured[0].threshold, 0.75);
});
});

});
27 changes: 12 additions & 15 deletions www/src/content/reference/01-attributes/07-hx-trigger.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,30 +62,27 @@ Fires when the element is loaded into the DOM. Useful for [lazy-loading](/patter
<div hx-trigger="load" hx-get="...">Loading...</div>
```

### `revealed`
### `intersect` / `revealed`

Fires when the element is scrolled into the viewport. Useful for [infinite scroll](/patterns/infinite-scroll).
Both use the [IntersectionObserver API](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver/IntersectionObserver#options) to fire when an element enters the viewport. The only difference is that `revealed` automatically disconnects the observer after the first intersection — it fires exactly once.

```html
<!-- fires every time the element intersects -->
<div hx-trigger="intersect" hx-get="...">...</div>

<!-- fires once, then stops observing -->
<div hx-trigger="revealed" hx-get="...">Loading...</div>
```

_Note: `revealed` always observes the browser viewport. For scrollable containers with `overflow`, use [`intersect`](#intersect) with `root` instead._

### `intersect`

Fires when an element becomes visible in the viewport.

Uses the [IntersectionObserver API](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver/IntersectionObserver#options).

Modifiers:
Modifiers (`intersect` and `revealed` both support these):

- [`root`](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver/root)
- [`rootMargin`](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver/rootMargin)
- [`threshold`](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver/thresholds)
- `from:<selector>` — observe a different element instead of the element itself
- [`root:<selector>`](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver/root) — use a scrollable container as the intersection root instead of the viewport
- [`rootMargin:<value>`](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver/rootMargin) — expand or shrink the root bounds
- [`threshold:<0–1>`](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver/thresholds) — minimum visible fraction before firing

```html
<div hx-trigger="intersect once" hx-get="...">...</div>
<div hx-trigger="intersect from:#other" hx-get="...">...</div>
<div hx-trigger="intersect root:#scroll-container" hx-get="...">...</div>
<div hx-trigger="intersect rootMargin:100px" hx-get="...">...</div>
<div hx-trigger="intersect threshold:0.5" hx-get="...">...</div>
Expand Down
19 changes: 12 additions & 7 deletions www/src/content/reference/03-events/33-intersect.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ title: "intersect"
description: "Fires when element enters viewport"
---

The `intersect` trigger event fires when an element enters the viewport, detected via IntersectionObserver.
The `intersect` event fires when an observed element enters the viewport, detected via IntersectionObserver. It is dispatched on the observed element.

`revealed` is identical except it disconnects the observer after the first intersection, so it fires exactly once.

## When It Fires

When an element becomes visible in the viewport, used by [`hx-trigger`](/reference/attributes/hx-trigger)`="intersect"`.
Used by [`hx-trigger`](/reference/attributes/hx-trigger)`="intersect"` and `hx-trigger="revealed"`. Also fires when `from:<selector>` is used on either — the event is dispatched on the `from` element when it intersects.

## Event Detail

Expand All @@ -16,15 +18,18 @@ Empty - no additional context provided.
## Example

```html
<div id="lazy" hx-get="/lazy-content" hx-trigger="intersect">
Content loads when scrolled into view
</div>
<!-- fires every time element intersects -->
<div hx-get="/lazy-content" hx-trigger="intersect">...</div>

<!-- fires once -->
<div hx-get="/lazy-content" hx-trigger="revealed">Loading...</div>

<!-- observe a different element -->
<div hx-get="/more" hx-trigger="intersect from:#sentinel">...</div>
```

```javascript
htmx.on('#lazy', 'intersect', (evt) => {
console.log('Element visible:', evt.target);
});
```

This enables lazy loading and infinite scroll patterns.
Loading