useInView
useInView returns a Svelte readable store that reports whether an element is
in the viewport. It uses the same IntersectionObserver infrastructure as the whileInView motion prop, so it’s a good fit for non-animation side effects:
analytics impressions, lazy data loads, one-shot reveals, and scroll-driven UI.
<script>
import { useInView } from '@humanspeak/svelte-motion'
let ref
const inView = useInView(() => ref)
</script>
<div bind:this={ref}>
{$inView ? 'visible' : 'hidden'}
</div><script>
import { useInView } from '@humanspeak/svelte-motion'
let ref
const inView = useInView(() => ref)
</script>
<div bind:this={ref}>
{$inView ? 'visible' : 'hidden'}
</div>target accepts either an HTMLElement directly or a getter () => HTMLElement | undefined. The getter form is the right choice with
Svelte 5 bind:this, because the element binding isn’t available until after
mount — the hook resolves it lazily and polls on requestAnimationFrame until it appears.
Latching with once
Pass { once: true } when you only care about the first viewport entry. The
store flips to true and stops observing — subsequent scrolls don’t flip
it back:
<script lang="ts">
import { useInView } from '@humanspeak/svelte-motion'
let card: HTMLElement | undefined
const trackImpression = () => {
// send analytics, fetch lazy data, etc.
}
const inView = useInView(() => card, { once: true })
$effect(() => {
if ($inView) trackImpression()
})
</script>
<article bind:this={card}>…</article><script lang="ts">
import { useInView } from '@humanspeak/svelte-motion'
let card: HTMLElement | undefined
const trackImpression = () => {
// send analytics, fetch lazy data, etc.
}
const inView = useInView(() => card, { once: true })
$effect(() => {
if ($inView) trackImpression()
})
</script>
<article bind:this={card}>…</article>Custom thresholds and roots
amount controls how much of the element must be visible to count as
“in view”: "some" (default, any pixel), "all" (fully visible), or a number
between 0 and 1. margin is forwarded to IntersectionObserver’s rootMargin, and root lets you observe inside a scrollable container instead
of the viewport.
<script>
import { useInView } from '@humanspeak/svelte-motion'
let scrollContainer
let target
const inView = useInView(() => target, {
root: () => scrollContainer,
amount: 0.5,
margin: '-10% 0%'
})
</script>
<div bind:this={scrollContainer} style="overflow: auto; height: 400px;">
<div bind:this={target}>…</div>
</div><script>
import { useInView } from '@humanspeak/svelte-motion'
let scrollContainer
let target
const inView = useInView(() => target, {
root: () => scrollContainer,
amount: 0.5,
margin: '-10% 0%'
})
</script>
<div bind:this={scrollContainer} style="overflow: auto; height: 400px;">
<div bind:this={target}>…</div>
</div>How it works
- Subscribes to
motion’sinView()primitive, which is also used bywhileInView— one IntersectionObserver implementation, two consumers. - Lazy-starts on the first store subscriber and stops when the last unsubscribes.
- Returns a static
readable(initial)whenwindoworIntersectionObserveris unavailable, so server rendering is safe.
API Reference
Parameters
- target
HTMLElement | (() => HTMLElement | undefined)— the element to observe. - options
UseInViewOptions(optional)
UseInViewOptions
| Option | Type | Default | Description |
|---|---|---|---|
root | HTMLElement \| () => HTMLElement | viewport | Scroll container to observe inside. |
margin | string | '0px' | CSS margin around the root bounding box (passed to rootMargin). |
amount | 'some' \| 'all' \| number | 'some' | Fraction of the target that must be visible. |
once | boolean | false | When true, latches true on first entry and stops observing. |
initial | boolean | false | Value emitted before the first IntersectionObserver callback. |
Returns
Readable<boolean> — subscribe with $inView or inView.subscribe(cb).
See also
- The
whileInViewmotion prop — declarative animation when amotion.*element enters the viewport. Both this hook andwhileInViewshare the sameIntersectionObserverinfrastructure under the hood. - useScroll — scroll position stores for scroll-driven animations.
Based on Motion’s useInView API.