{"_id":"@ariefsn/svelte-sentinel","name":"@ariefsn/svelte-sentinel","dist-tags":{"latest":"1.0.0"},"versions":{"1.0.0":{"name":"@ariefsn/svelte-sentinel","version":"1.0.0","description":"A lightweight Svelte 5 component that wraps IntersectionObserver for declarative viewport visibility detection.","license":"MIT","repository":{"type":"git","url":"git+https://github.com/ariefsn/svelte-sentinel.git"},"homepage":"https://github.com/ariefsn/svelte-sentinel#readme","bugs":{"url":"https://github.com/ariefsn/svelte-sentinel/issues"},"main":"./dist/index.js","scripts":{"dev":"vite dev","build":"vite build && bun run prepack","preview":"vite preview","prepare":"svelte-kit sync || echo ''","prepack":"svelte-kit sync && svelte-package && publint","check":"svelte-kit sync && svelte-check --tsconfig ./tsconfig.json","check:watch":"svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch","lint":"prettier --check . && eslint .","format":"prettier --write .","test:unit":"vitest","test":"bun run test:unit -- --run"},"sideEffects":["**/*.css"],"svelte":"./dist/index.js","types":"./dist/index.d.ts","type":"module","exports":{".":{"types":"./dist/index.d.ts","svelte":"./dist/index.js"}},"peerDependencies":{"svelte":"^5.0.0"},"devDependencies":{"@eslint/compat":"^2.0.2","@eslint/js":"^9.39.2","@sveltejs/adapter-auto":"^7.0.0","@sveltejs/kit":"^2.50.2","@sveltejs/package":"^2.5.7","@sveltejs/vite-plugin-svelte":"^6.2.4","@types/node":"^22","@vitest/browser-playwright":"^4.0.18","eslint":"^9.39.2","eslint-config-prettier":"^10.1.8","eslint-plugin-svelte":"^3.14.0","globals":"^17.3.0","playwright":"^1.58.1","prettier":"^3.8.1","prettier-plugin-svelte":"^3.4.1","publint":"^0.3.17","svelte":"^5.51.0","svelte-check":"^4.3.6","typescript":"^5.9.3","typescript-eslint":"^8.54.0","vite":"^7.3.1","vitest":"^4.0.18","vitest-browser-svelte":"^2.0.2"},"keywords":["svelte","svelte5","intersection-observer","visibility","viewport","scroll","lazy-load","animate-on-scroll","in-view","sentinel"],"_id":"@ariefsn/svelte-sentinel@1.0.0","gitHead":"7ef5adffc6b121dd1c68101d9f8214a2cf571ad5","_nodeVersion":"22.20.0","_npmVersion":"10.9.3","dist":{"integrity":"sha512-9+EehJPo17x2GPYa0msB+IXK9fREyXjxBHaB99FUSZSK2q0QfV0tujALnMEjolEO2NGLKl2dmOpulONZrIBL1Q==","shasum":"1f31ec680c530c275a50556add9c3e86511df4fd","tarball":"https://registry.npmjs.org/@ariefsn/svelte-sentinel/-/svelte-sentinel-1.0.0.tgz","fileCount":7,"unpackedSize":20084,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCID9nQEVuhVOEwqMoAOgKadXZthisGjRrewAOHyzEWOnJAiBJ7YiwUjKFa8rjHhEP7iOU16Nlc8ySkdLML2yhJ7DMhQ=="}]},"_npmUser":{"name":"ariefsn","email":"ayiexz22@gmail.com"},"directories":{},"maintainers":[{"name":"ariefsn","email":"ayiexz22@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/svelte-sentinel_1.0.0_1772158481447_0.455542426443287"},"_hasShrinkwrap":false}},"time":{"created":"2026-02-27T02:14:41.364Z","1.0.0":"2026-02-27T02:14:41.594Z","modified":"2026-02-27T02:14:41.789Z"},"maintainers":[{"name":"ariefsn","email":"ayiexz22@gmail.com"}],"description":"A lightweight Svelte 5 component that wraps IntersectionObserver for declarative viewport visibility detection.","homepage":"https://github.com/ariefsn/svelte-sentinel#readme","keywords":["svelte","svelte5","intersection-observer","visibility","viewport","scroll","lazy-load","animate-on-scroll","in-view","sentinel"],"repository":{"type":"git","url":"git+https://github.com/ariefsn/svelte-sentinel.git"},"bugs":{"url":"https://github.com/ariefsn/svelte-sentinel/issues"},"license":"MIT","readme":"# Svelte Sentinel\n\n![svelte-sentinel feature banner](./static/feature.svg)\n\n[![npm](https://img.shields.io/npm/v/@ariefsn/svelte-sentinel?color=ff3e00&logo=npm)](https://www.npmjs.com/package/@ariefsn/svelte-sentinel)\n[![GitHub](https://img.shields.io/badge/GitHub-ariefsn%2Fsvelte--sentinel-181717?logo=github)](https://github.com/ariefsn/svelte-sentinel)\n[![license](https://img.shields.io/badge/license-MIT-blue)](./LICENSE.md)\n\nA lightweight **Svelte 5** component that wraps the browser's [`IntersectionObserver`](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API) API, letting you declaratively respond to an element entering or leaving the viewport.\n\n- Zero external dependencies\n- Svelte 5 runes (`$props`, `$state`, `$bindable`)\n- Full TypeScript support\n- Callbacks, bindable state, and conditional snippet slots\n\n---\n\n## Installation\n\n```bash\n# npm\nnpm install @ariefsn/svelte-sentinel\n\n# pnpm\npnpm add @ariefsn/svelte-sentinel\n\n# yarn\nyarn add @ariefsn/svelte-sentinel\n\n# bun\nbun add @ariefsn/svelte-sentinel\n```\n\n**Peer dependency:** Svelte 5 (`^5.0.0`)\n\n---\n\n## Quick Start\n\n```svelte\n<script>\n  import { Sentinel } from '@ariefsn/svelte-sentinel';\n</script>\n\n<Sentinel onEnter={() => console.log('in view!')} onExit={() => console.log('out of view!')}>\n  <p>Watch me scroll!</p>\n</Sentinel>\n```\n\n---\n\n## Usage\n\n### Basic visibility callbacks\n\nUse `onEnter`, `onExit`, or `onViewChange` to react to visibility changes.\nEach callback receives the raw [`IntersectionObserverEntry`](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserverEntry) for full control.\n\n```svelte\n<Sentinel\n  onEnter={(entry) => console.log('entered', entry.intersectionRatio)}\n  onExit={() => console.log('exited')}\n  onViewChange={(isVisible) => console.log('visible:', isVisible)}\n>\n  <div>Observed element</div>\n</Sentinel>\n```\n\n---\n\n### Reactive state with `bind:isVisible`\n\nBind the `isVisible` prop to read the element's visibility reactively anywhere in the parent component — no callback needed.\n\n```svelte\n<script>\n  import { Sentinel } from '@ariefsn/svelte-sentinel';\n\n  let visible = $state(false);\n</script>\n\n<header class:scrolled={!visible}>Sticky header</header>\n\n<Sentinel bind:isVisible={visible}>\n  <h1>Page hero</h1>\n</Sentinel>\n```\n\n---\n\n### Animate on scroll (one-shot with `once`)\n\nSet `once={true}` to disconnect the observer after the element becomes visible for the first time. Perfect for entrance animations that should only play once.\n\n```svelte\n<script>\n  import { Sentinel } from '@ariefsn/svelte-sentinel';\n</script>\n\n<style>\n  .box { opacity: 0; transform: translateY(2rem); transition: opacity .5s, transform .5s; }\n  .box.visible { opacity: 1; transform: none; }\n</style>\n\n<Sentinel once>\n  {#snippet whenVisible()}\n    <div class=\"box visible\">I animated in!</div>\n  {/snippet}\n  {#snippet whenHidden()}\n    <div class=\"box\">Waiting…</div>\n  {/snippet}\n</Sentinel>\n```\n\n---\n\n### Conditional content with `whenVisible` / `whenHidden`\n\nUse the `whenVisible` and `whenHidden` snippet slots to declaratively swap content based on viewport position. No state management required in the parent.\n\n```svelte\n<Sentinel>\n  {#snippet whenVisible()}\n    <span class=\"badge green\">In view</span>\n  {/snippet}\n  {#snippet whenHidden()}\n    <span class=\"badge grey\">Out of view</span>\n  {/snippet}\n</Sentinel>\n```\n\n> **Note:** `children` is always rendered and is independent of `whenVisible` / `whenHidden`. Use `children` for static content that should always be inside the wrapper.\n\n---\n\n### Lazy loading with `rootMargin`\n\n`rootMargin` expands the detection area beyond the viewport — useful for pre-loading images or data before they scroll into view.\n\n```svelte\n<script>\n  import { Sentinel } from '@ariefsn/svelte-sentinel';\n  let loaded = $state(false);\n</script>\n\n<!-- Start loading 300px before the image enters the viewport -->\n<Sentinel rootMargin=\"300px\" once onEnter={() => (loaded = true)}>\n  {#if loaded}\n    <img src=\"/photo.jpg\" alt=\"Lazy loaded\" />\n  {:else}\n    <div class=\"placeholder\">Loading…</div>\n  {/if}\n</Sentinel>\n```\n\n---\n\n### Custom scroll container with `root`\n\nObserve visibility inside a scrollable container instead of the browser viewport.\n\n```svelte\n<script>\n  import { Sentinel } from '@ariefsn/svelte-sentinel';\n  let container: HTMLElement;\n</script>\n\n<div bind:this={container} style=\"overflow-y: scroll; height: 400px;\">\n  <Sentinel root={container} onEnter={() => console.log('visible inside container')}>\n    <div style=\"margin-top: 600px;\">Deep content</div>\n  </Sentinel>\n</div>\n```\n\n---\n\n### Custom wrapper tag with `as`\n\nChange the wrapper element's HTML tag to fit your semantic HTML.\n\n```svelte\n<Sentinel as=\"section\" class=\"hero-section\" onEnter={handleEnter}>\n  <h1>Hero Section</h1>\n</Sentinel>\n\n<!-- Renders as: <section class=\"hero-section\">…</section> -->\n```\n\n---\n\n### Infinite scroll\n\nUse a small invisible sentinel at the end of a list to trigger loading the next page.\n\n```svelte\n<script>\n  import { Sentinel } from '@ariefsn/svelte-sentinel';\n\n  let items = $state(Array.from({ length: 20 }, (_, i) => i + 1));\n  let loading = $state(false);\n\n  async function loadMore() {\n    if (loading) return;\n    loading = true;\n    await new Promise((r) => setTimeout(r, 500)); // simulate fetch\n    items = [...items, ...Array.from({ length: 20 }, (_, i) => items.length + i + 1)];\n    loading = false;\n  }\n</script>\n\n<ul>\n  {#each items as item}\n    <li>{item}</li>\n  {/each}\n</ul>\n\n{#if loading}\n  <p>Loading…</p>\n{/if}\n\n<!-- Invisible sentinel at the bottom of the list -->\n<Sentinel onEnter={loadMore} />\n```\n\n---\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `id` | `string` | auto-generated | `id` attribute on the wrapper element. |\n| `class` | `string` | `undefined` | CSS class(es) for the wrapper element. |\n| `as` | `string` | `'div'` | HTML tag for the wrapper element (e.g. `'section'`, `'li'`). |\n| `threshold` | `number \\| number[]` | `0` | When to trigger: `0` = any pixel visible, `1` = fully visible, or an array of ratios. |\n| `rootMargin` | `string` | `'0px'` | CSS margin around the root. Positive values expand, negative values shrink the detection area. |\n| `root` | `Element \\| Document \\| null` | `null` | Scroll container to use instead of the browser viewport. |\n| `once` | `boolean` | `false` | Disconnect the observer after the element first becomes visible. |\n| `isVisible` | `boolean` | `false` | Bindable. Use `bind:isVisible` to reactively track visibility. |\n| `onEnter` | `(entry: IntersectionObserverEntry) => void` | — | Called when the element enters the viewport. |\n| `onExit` | `(entry: IntersectionObserverEntry) => void` | — | Called when the element exits the viewport. |\n| `onViewChange` | `(isVisible: boolean, entry: IntersectionObserverEntry) => void` | — | Called on every visibility change. |\n| `children` | `Snippet` | — | Always-rendered content inside the wrapper. |\n| `whenVisible` | `Snippet` | — | Rendered only while the element is visible. |\n| `whenHidden` | `Snippet` | — | Rendered only while the element is not visible. |\n\n---\n\n## TypeScript\n\nAll types are exported from the package entry point.\n\n```ts\nimport type { SentinelProps } from '@ariefsn/svelte-sentinel';\n```\n\n---\n\n## Developing\n\n```bash\n# Install dependencies\nbun install\n\n# Start the dev server (runs the demo app)\nbun run dev\n\n# Run tests\nbun run test\n\n# Type-check\nbun run check\n\n# Build the library\nbun run prepack\n```\n\n---\n\n## License\n\n[MIT](./LICENSE.md) © [ariefsn](https://github.com/ariefsn)\n\n---\n\n[GitHub](https://github.com/ariefsn/svelte-sentinel) · [npm](https://www.npmjs.com/package/@ariefsn/svelte-sentinel)\n","readmeFilename":"README.md","_rev":"1-669fde6f96ca854ea224eb9d8c557d53"}