{"id":48555832,"url":"https://github.com/ocramz/htmx-intersect","last_synced_at":"2026-04-08T11:01:33.424Z","repository":{"id":338762556,"uuid":"1159049326","full_name":"ocramz/htmx-intersect","owner":"ocramz","description":"HTMX extension for IntersectionObserver","archived":false,"fork":false,"pushed_at":"2026-02-16T09:39:05.000Z","size":61,"stargazers_count":0,"open_issues_count":3,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-03-25T00:24:52.368Z","etag":null,"topics":["htmx-extension","intersection-observer","scrollytelling"],"latest_commit_sha":null,"homepage":"","language":"HTML","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ocramz.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-02-16T08:52:15.000Z","updated_at":"2026-02-16T09:39:09.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/ocramz/htmx-intersect","commit_stats":null,"previous_names":["ocramz/htmx-intersect"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/ocramz/htmx-intersect","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ocramz%2Fhtmx-intersect","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ocramz%2Fhtmx-intersect/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ocramz%2Fhtmx-intersect/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ocramz%2Fhtmx-intersect/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ocramz","download_url":"https://codeload.github.com/ocramz/htmx-intersect/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ocramz%2Fhtmx-intersect/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31551891,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-08T10:21:54.569Z","status":"ssl_error","status_checked_at":"2026-04-08T10:21:38.171Z","response_time":54,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["htmx-extension","intersection-observer","scrollytelling"],"created_at":"2026-04-08T11:01:32.647Z","updated_at":"2026-04-08T11:01:33.414Z","avatar_url":"https://github.com/ocramz.png","language":"HTML","funding_links":[],"categories":[],"sub_categories":[],"readme":"# htmx-intersect\n\n[![Playwright Tests](https://github.com/ocramz/htmx-intersect/actions/workflows/test.yml/badge.svg)](https://github.com/ocramz/htmx-intersect/actions/workflows/test.yml)\n\nA lightweight HTMX extension that integrates the Intersection Observer API to simplify scroll-based web experiences like lazy loading, infinite scroll, and visibility tracking.\n\n## Features\n\n- 🚀 **Simple API** - Just add attributes to your HTML\n- 🎯 **Lazy Loading** - Load content only when visible\n- ♾️ **Infinite Scroll** - Automatically load more content\n- 👁️ **Visibility Tracking** - Track when elements enter/exit viewport\n- 🎨 **No Dependencies** - Works with vanilla HTMX\n- ⚡ **Performant** - Uses native IntersectionObserver API\n- 🔄 **Reusable Observers** - Shares observers across elements with same config\n\n## Installation\n\n### Via CDN\n\n```html\n\u003cscript src=\"https://unpkg.com/htmx.org@1.9.10\"\u003e\u003c/script\u003e\n\u003cscript src=\"htmx-intersect.js\"\u003e\u003c/script\u003e\n```\n\n### Via npm\n\n```bash\nnpm install htmx-intersect\n```\n\n```javascript\nimport 'htmx-intersect';\n```\n\n## Quick Start\n\n### Basic Usage\n\n```html\n\u003cdiv hx-ext=\"intersect\"\n     hx-get=\"/api/content\"\n     hx-trigger=\"intersect\"\u003e\n  This content will load when scrolled into view\n\u003c/div\u003e\n```\n\n### Lazy Loading Images\n\n```html\n\u003cimg hx-ext=\"intersect\"\n     hx-get=\"/api/image/123\"\n     hx-trigger=\"intersect once\"\n     hx-swap=\"outerHTML\"\n     src=\"placeholder.jpg\"\n     alt=\"Lazy loaded image\"\u003e\n```\n\n### Infinite Scroll\n\n```html\n\u003cdiv id=\"content\"\u003e\n  \u003c!-- Your content here --\u003e\n\u003c/div\u003e\n\n\u003c!-- Trigger element at the bottom --\u003e\n\u003cdiv hx-ext=\"intersect\"\n     hx-get=\"/api/more?page=2\"\n     hx-trigger=\"intersect once\"\n     hx-target=\"#content\"\n     hx-swap=\"beforeend\"\n     intersect-threshold=\"0.5\"\u003e\n  Loading more...\n\u003c/div\u003e\n```\n\n## Configuration Attributes\n\n### Core Attributes\n\n#### `hx-trigger=\"intersect\"`\nTriggers an HTMX request when the element intersects with the viewport.\n\n**Modifiers:**\n- `once` - Trigger only the first time (perfect for lazy loading)\n- Example: `hx-trigger=\"intersect once\"`\n\n### Extension-Specific Attributes\n\n#### `intersect-root`\nSpecifies the root element to observe intersection against. If not set or set to `null` or `viewport`, uses the browser viewport.\n\n```html\n\u003cdiv intersect-root=\"#scrollContainer\"\u003e\n  Observes intersection within #scrollContainer\n\u003c/div\u003e\n```\n\n#### `intersect-threshold`\nThe percentage of the element that must be visible to trigger. Can be:\n- Single value: `0.5` (50% visible)\n- Multiple values: `0,0.25,0.5,0.75,1` (triggers at each threshold)\n\n```html\n\u003c!-- Trigger when 50% visible --\u003e\n\u003cdiv intersect-threshold=\"0.5\"\u003e\n\n\u003c!-- Trigger at 0%, 50%, and 100% --\u003e\n\u003cdiv intersect-threshold=\"0,0.5,1.0\"\u003e\n```\n\n**Default:** `0` (triggers as soon as any pixel is visible)\n\n#### `intersect-margin`\nMargin around the root element (similar to CSS margin). Positive values expand the root's area, negative values shrink it.\n\n```html\n\u003c!-- Load content 200px before it enters viewport --\u003e\n\u003cdiv intersect-margin=\"200px 0px 0px 0px\"\u003e\n\n\u003c!-- Percentage-based --\u003e\n\u003cdiv intersect-margin=\"10%\"\u003e\n```\n\n**Default:** `\"0px\"`\n\n#### `intersect-scroll-margin`\nMargin around nested scroll containers. Useful when you have scrollable elements within the root.\n\n```html\n\u003cdiv intersect-scroll-margin=\"50px\"\u003e\n```\n\n**Default:** `\"0px\"`\n\n#### `intersect-unload`\nControls whether and how to unload/remove elements when they exit the viewport. Great for memory management in infinite scroll scenarios.\n\n**Values:**\n- `\"true\"` or `\"remove\"` - Completely remove element from DOM\n- `\"content\"` - Remove only innerHTML, keep element shell (content restored on re-entry)\n- `\"hide\"` - Set `display: none` (faster than removal)\n- `\"false\"` or omit - No unloading (default)\n\n```html\n\u003c!-- Remove element when it exits viewport --\u003e\n\u003cdiv intersect-unload=\"true\"\u003e\n\n\u003c!-- Just hide the element --\u003e\n\u003cdiv intersect-unload=\"hide\"\u003e\n\n\u003c!-- Remove content but keep element --\u003e\n\u003cdiv intersect-unload=\"content\" \n     intersect-unload-placeholder=\"\u003cdiv\u003eLoading...\u003c/div\u003e\"\u003e\n```\n\n**Default:** Not set (no unloading)\n\n#### `intersect-unload-delay`\nDelay in milliseconds before unloading. Prevents flickering when scrolling quickly.\n\n```html\n\u003c!-- Wait 2 seconds before unloading --\u003e\n\u003cdiv intersect-unload=\"true\" \n     intersect-unload-delay=\"2000\"\u003e\n```\n\n**Default:** `0` (immediate)\n\n#### `intersect-unload-placeholder`\nHTML to show when using `intersect-unload=\"content\"`. Only used with content mode.\n\n```html\n\u003cdiv intersect-unload=\"content\"\n     intersect-unload-placeholder=\"\u003cdiv class='skeleton'\u003eLoading...\u003c/div\u003e\"\u003e\n```\n\n## Events\n\nThe extension emits custom events you can listen to:\n\n### `intersect:enter`\nFired when element enters the viewport.\n\n```javascript\nelement.addEventListener('intersect:enter', (event) =\u003e {\n  console.log('Element entered!', event.detail);\n  // detail: { ratio, time, bounds }\n});\n```\n\n### `intersect:exit`\nFired when element exits the viewport.\n\n```javascript\nelement.addEventListener('intersect:exit', (event) =\u003e {\n  console.log('Element exited!', event.detail);\n  // detail: { ratio, time }\n});\n```\n\n### `intersect:visible`\nContinuously fired with visibility updates.\n\n```javascript\nelement.addEventListener('intersect:visible', (event) =\u003e {\n  console.log('Visibility ratio:', event.detail.ratio);\n  // detail: { ratio, isIntersecting }\n});\n```\n\n### `intersect:beforeunload`\nFired before an element is unloaded (when using `intersect-unload`). Can be prevented.\n\n```javascript\nelement.addEventListener('intersect:beforeunload', (event) =\u003e {\n  console.log('About to unload', event.detail.mode);\n  // Prevent unloading if needed\n  if (shouldKeepElement) {\n    event.preventDefault();\n  }\n});\n```\n\n### `intersect:unload`\nFired after an element is unloaded. Fired on the parent element.\n\n```javascript\nparent.addEventListener('intersect:unload', (event) =\u003e {\n  console.log('Element unloaded:', event.detail.element);\n  // detail: { mode, element }\n});\n```\n\n## Use Cases \u0026 Examples\n\n### 1. Lazy Loading Components\n\n```html\n\u003cdiv class=\"lazy-component\"\n     hx-ext=\"intersect\"\n     hx-get=\"/components/widget\"\n     hx-trigger=\"intersect once\"\n     intersect-threshold=\"0.1\"\u003e\n  \u003cdiv class=\"skeleton-loader\"\u003eLoading...\u003c/div\u003e\n\u003c/div\u003e\n```\n\n### 2. Infinite Scroll with Loading Indicator\n\n```html\n\u003cdiv id=\"posts\"\u003e\n  \u003c!-- Posts loaded here --\u003e\n\u003c/div\u003e\n\n\u003cdiv hx-ext=\"intersect\"\n     hx-get=\"/api/posts\"\n     hx-trigger=\"intersect once\"\n     hx-target=\"#posts\"\n     hx-swap=\"beforeend\"\n     hx-indicator=\"#loading\"\n     intersect-margin=\"300px 0px 0px 0px\"\u003e\n  \u003cdiv id=\"loading\" class=\"htmx-indicator\"\u003e\n    \u003cspan\u003eLoading more posts...\u003c/span\u003e\n  \u003c/div\u003e\n\u003c/div\u003e\n```\n\n### 3. Analytics Tracking\n\n```html\n\u003cdiv hx-ext=\"intersect\"\n     hx-post=\"/analytics/view\"\n     hx-trigger=\"intersect once\"\n     intersect-threshold=\"0.5\"\n     data-content-id=\"article-123\"\u003e\n  Article content here\n\u003c/div\u003e\n```\n\n### 4. Progressive Image Loading\n\n```html\n\u003cpicture hx-ext=\"intersect\"\n         hx-get=\"/images/high-res/photo.jpg\"\n         hx-trigger=\"intersect once\"\n         hx-swap=\"outerHTML\"\n         intersect-margin=\"100px\"\u003e\n  \u003cimg src=\"low-res-placeholder.jpg\" alt=\"Photo\"\u003e\n\u003c/picture\u003e\n```\n\n### 5. Video Autoplay on Scroll\n\n```html\n\u003cvideo hx-ext=\"intersect\"\n       src=\"video.mp4\"\n       data-hx-on:intersect:enter=\"this.play()\"\n       data-hx-on:intersect:exit=\"this.pause()\"\u003e\n\u003c/video\u003e\n```\n\n### 6. Sticky Header Detection\n\n```html\n\u003cdiv id=\"header\" \n     hx-ext=\"intersect\"\n     intersect-threshold=\"0,1\"\n     data-hx-on:intersect:visible=\"\n       if (event.detail.ratio \u003c 1) {\n         this.classList.add('sticky');\n       } else {\n         this.classList.remove('sticky');\n       }\n     \"\u003e\n  Header content\n\u003c/div\u003e\n```\n\n### 7. Content Loading in Scrollable Container\n\n```html\n\u003cdiv id=\"scrollContainer\" style=\"height: 400px; overflow-y: auto;\"\u003e\n  \u003cdiv hx-ext=\"intersect\"\n       hx-get=\"/nested/content\"\n       hx-trigger=\"intersect once\"\n       intersect-root=\"#scrollContainer\"\n       intersect-threshold=\"0.5\"\u003e\n    Nested scrollable content\n  \u003c/div\u003e\n\u003c/div\u003e\n```\n\n### 8. Memory-Efficient Infinite Scroll (Unload Off-Screen Content)\n\n```html\n\u003cdiv id=\"posts\"\u003e\u003c/div\u003e\n\n\u003c!-- Load new content --\u003e\n\u003cdiv hx-ext=\"intersect\"\n     hx-get=\"/api/posts?page=2\"\n     hx-trigger=\"intersect once\"\n     hx-target=\"#posts\"\n     hx-swap=\"beforeend\"\n     intersect-margin=\"500px\"\u003e\n  Loading more...\n\u003c/div\u003e\n\n\u003c!-- Inside each post, enable unloading --\u003e\n\u003cdiv class=\"post\"\n     hx-ext=\"intersect\"\n     intersect-unload=\"content\"\n     intersect-unload-delay=\"1000\"\n     intersect-unload-placeholder=\"\u003cdiv class='skeleton'\u003ePost removed from memory\u003c/div\u003e\"\u003e\n  Post content here...\n\u003c/div\u003e\n```\n\n### 9. Virtual Scrolling with Content Unloading\n\n```html\n\u003c!-- Each item unloads when far from viewport --\u003e\n\u003cdiv class=\"list-item\"\n     hx-ext=\"intersect\"\n     hx-trigger=\"intersect\"\n     intersect-unload=\"content\"\n     intersect-margin=\"1000px\"\n     intersect-unload-placeholder=\"\u003cdiv class='placeholder'\u003eItem #{id}\u003c/div\u003e\"\u003e\n  Heavy content here...\n\u003c/div\u003e\n```\n\n### 10. Remove Ads After Viewing\n\n```html\n\u003cdiv class=\"advertisement\"\n     hx-ext=\"intersect\"\n     hx-post=\"/analytics/ad-viewed\"\n     hx-trigger=\"intersect once\"\n     intersect-threshold=\"0.5\"\n     intersect-unload=\"true\"\n     intersect-unload-delay=\"5000\"\u003e\n  Ad content (removed 5s after leaving viewport)\n\u003c/div\u003e\n```\n\n## Advanced Usage\n\n### Manual Observer Control\n\nFor complex scenarios, you can use the JavaScript API:\n\n```javascript\n// Start observing an element\nhtmx.intersect.observe(element);\n\n// Stop observing\nhtmx.intersect.unobserve(element);\n\n// Create custom observer\nconst observer = htmx.intersect.createObserver(\n  {\n    root: null,\n    rootMargin: '0px',\n    threshold: [0, 0.5, 1]\n  },\n  (entries) =\u003e {\n    entries.forEach(entry =\u003e {\n      console.log('Intersection:', entry);\n    });\n  }\n);\n\nobserver.observe(element);\n```\n\n### Combining with Other HTMX Features\n\n```html\n\u003c!-- Intersect + Polling --\u003e\n\u003cdiv hx-ext=\"intersect\"\n     hx-get=\"/live-data\"\n     hx-trigger=\"intersect once, every 5s\"\u003e\n  Start polling when visible\n\u003c/div\u003e\n\n\u003c!-- Intersect + WebSocket --\u003e\n\u003cdiv hx-ext=\"intersect,ws\"\n     ws-connect=\"/live-feed\"\n     hx-trigger=\"intersect once\"\u003e\n  Connect to WebSocket when visible\n\u003c/div\u003e\n\n\u003c!-- Intersect + Animation --\u003e\n\u003cdiv hx-ext=\"intersect\"\n     class=\"fade-in-element\"\n     data-hx-on:intersect:enter=\"this.classList.add('visible')\"\u003e\n  Animated content\n\u003c/div\u003e\n```\n\n## CSS Classes\n\nThe extension automatically adds/removes the `intersecting` class:\n\n```css\n.my-element {\n  opacity: 0;\n  transform: translateY(50px);\n  transition: all 0.6s ease;\n}\n\n.my-element.intersecting {\n  opacity: 1;\n  transform: translateY(0);\n}\n```\n\n## Browser Support\n\nWorks in all browsers that support:\n- HTMX 1.9+\n- IntersectionObserver API (all modern browsers)\n\nFor older browsers, consider using a [polyfill](https://github.com/w3c/IntersectionObserver/tree/main/polyfill).\n\n## Performance Considerations\n\n1. **Use `once` modifier** for one-time loads to automatically clean up observers\n2. **Set appropriate thresholds** - Don't use too many threshold values\n3. **Use root margins wisely** - Preload content just before it's needed\n4. **Shared observers** - Elements with identical configs share observers\n\n## Troubleshooting\n\n### Element not triggering\n- Ensure `hx-trigger=\"intersect\"` is set\n- Check that element has non-zero dimensions\n- Verify `intersect-threshold` is appropriate\n- Check browser console for errors\n\n### Multiple triggers\n- Add `once` modifier: `hx-trigger=\"intersect once\"`\n- Or use `intersect-threshold` to be more specific\n\n### Not working in nested scrollers\n- Set `intersect-root` to the scroll container\n- Consider using `intersect-scroll-margin`\n\n## Contributing\n\nContributions welcome! Please open an issue or PR.\n\n## License\n\nMIT License - see LICENSE file for details\n\n## Credits\n\nBuilt with ❤️ for the HTMX community\n\n- [HTMX](https://htmx.org/)\n- [Intersection Observer API](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Focramz%2Fhtmx-intersect","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Focramz%2Fhtmx-intersect","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Focramz%2Fhtmx-intersect/lists"}