useVisibility
Whether an element is on screen, without state updates, so animation loops can skip frames nobody sees.
Every audiocn-svelte meter and visualizer that paints on animation frames uses this hook to stop painting while it is scrolled out of view.
$ pnpm dlx shadcn-svelte@latest add https://juddisjudd.github.io/audiocn-svelte/r/use-visibility.json$ npx shadcn-svelte@latest add https://juddisjudd.github.io/audiocn-svelte/r/use-visibility.json$ yarn dlx shadcn-svelte@latest add https://juddisjudd.github.io/audiocn-svelte/r/use-visibility.json$ bunx --bun shadcn-svelte@latest add https://juddisjudd.github.io/audiocn-svelte/r/use-visibility.jsonUsage
<script lang="ts">
import { useVisibility } from "#lib/hooks/use-visibility.svelte.js";
let element = $state<HTMLDivElement | null>(null);
const visible = useVisibility(() => element);
$effect(() => {
let frame = requestAnimationFrame(function tick(now) {
if (visible.current) {
element?.style.setProperty("--angle", `${(now / 10) % 360}deg`);
}
frame = requestAnimationFrame(tick);
});
return () => cancelAnimationFrame(frame);
});
</script>
<div bind:this={element} class="size-4 rotate-(--angle) bg-primary"></div>Call it during component setup, with the element as a getter. It returns an object whose current is not reactive: reading visible.current inside a loop costs nothing and never triggers an update. It reads true until the first observation, and stays true where IntersectionObserver is unavailable.
Waking a sleeping loop
Pass onChange to hear when the element comes into view or leaves it. audiocn-svelte painters use it with createFrameTask from #lib/audio/frame-loop.js: a meter that is off screen, or has settled, requests no frames at all, and coming back into view wakes it.
let task: FrameTask | null = null;
const visible = useVisibility(
() => element,
(isVisible) => {
if (isVisible) {
task?.wake();
}
}
);