Lightweight scroll animations for Svelte 5 ā Built with Svelte 5 Runes and IntersectionObserver API.
š Open Source by ludoloops at LeLab.dev š Licensed under MIT
prefers-reduced-motionnpm install rune-scroller
<script>
import runeScroller from 'rune-scroller';
</script>
<!-- Simple animation -->
<div use:runeScroller={{ animation: 'fade-in' }}>
<h2>Animated Heading</h2>
</div>
<!-- With custom duration -->
<div use:runeScroller={{ animation: 'fade-in-up', duration: 1500 }}>
<div class="card">Smooth fade and slide</div>
</div>
<!-- Repeat on every scroll -->
<div use:runeScroller={{ animation: 'bounce-in', repeat: true }}>
<button>Bounces on every scroll</button>
</div>
fade-in - Simple opacity fadefade-in-up - Fade + move up 300pxfade-in-down - Fade + move down 300pxfade-in-left - Fade + move from right 300pxfade-in-right - Fade + move from left 300pxzoom-in - Scale from 0.3 to 1zoom-out - Scale from 2 to 1zoom-in-up - Zoom (0.5ā1) + move up 300pxzoom-in-left - Zoom (0.5ā1) + move from right 300pxzoom-in-right - Zoom (0.5ā1) + move from left 300pxflip - 3D flip on Y-axisflip-x - 3D flip on X-axisslide-rotate - Slide + rotate 10°bounce-in - Bouncy entrance (spring effect)interface RuneScrollerOptions {
animation?: AnimationType // Animation name (default: 'fade-in')
duration?: number // Duration in ms (default: 800)
repeat?: boolean // Repeat on scroll (default: false)
debug?: boolean // Show sentinel as visible line (default: false)
offset?: number // Sentinel offset in px (default: 0, negative = earlier)
onVisible?: (element: HTMLElement) => void // Callback when visible
sentinelColor?: string // Debug sentinel color (e.g. '#ff6b6b')
sentinelId?: string // Custom sentinel ID
}
<!-- Basic -->
<div use:runeScroller={{ animation: 'zoom-in' }}>Content</div>
<!-- Custom duration -->
<div use:runeScroller={{ animation: 'fade-in-up', duration: 1000 }}>Fast</div>
<!-- Repeat mode -->
<div use:runeScroller={{ animation: 'bounce-in', repeat: true }}>Repeats</div>
<!-- Debug mode -->
<div use:runeScroller={{ animation: 'fade-in', debug: true }}>Debug</div>
<!-- Trigger earlier with negative offset -->
<div use:runeScroller={{ animation: 'fade-in-up', offset: -200 }}>
Triggers 200px before element bottom
</div>
<!-- onVisible callback for analytics -->
<div use:runeScroller={{
animation: 'fade-in-up',
onVisible: (el) => {
window.gtag?.('event', 'section_viewed', { id: el.id });
}
}}>
Tracked section
</div>
Sentinel-based triggering:
Why sentinels?
Works seamlessly with SvelteKit:
<!-- src/routes/+layout.svelte -->
<script>
import runeScroller from 'rune-scroller';
let { children } = $props();
</script>
{@render children()}
Respects prefers-reduced-motion:
@media (prefers-reduced-motion: reduce) {
.scroll-animate {
animation: none !important;
opacity: 1 !important;
transform: none !important;
}
}
// Default export
import runeScroller from "rune-scroller"
// Named exports
import {
useIntersection, // Composable
useIntersectionOnce, // Composable
calculateRootMargin, // Utility
} from "rune-scroller"
// Types
import type { AnimationType, RuneScrollerOptions } from "rune-scroller"
<script>
import runeScroller from 'rune-scroller';
const items = ['Item 1', 'Item 2', 'Item 3'];
</script>
{#each items as item, i}
<div use:runeScroller={{
animation: 'fade-in-up',
duration: 800,
style: `--delay: ${i * 100}ms`
}}>
{item}
</div>
{/each}
<h1 use:runeScroller={{ animation: 'fade-in-down', duration: 1000 }}>Welcome</h1>
<p use:runeScroller={{ animation: 'fade-in-up', duration: 1200 }}>Subtitle</p>
<button use:runeScroller={{ animation: 'zoom-in', duration: 800 }}>Get Started</button>
MIT Ā© ludoloops
Made with ā¤ļø by LeLab.dev