rune-scroller Svelte Themes

Rune Scroller

Lightweight, high-performance scroll animations for Svelte 5

⚔ Rune Scroller

Rune Scroller Logo

Lightweight scroll animations for Svelte 5 — Built with Svelte 5 Runes and IntersectionObserver API.

šŸš€ Open Source by ludoloops at LeLab.dev šŸ“œ Licensed under MIT


✨ Features

  • ~3.4KB gzipped (11.5KB uncompressed) - Minimal overhead
  • Zero dependencies - Pure Svelte 5 + IntersectionObserver
  • 14 animations - Fade, Zoom, Flip, Slide, Bounce variants
  • Full TypeScript support - Type definitions generated from JSDoc
  • SSR-ready - SvelteKit compatible
  • GPU-accelerated - Pure CSS transforms
  • Accessible - Respects prefers-reduced-motion

šŸ“¦ Installation

npm install rune-scroller
# or
pnpm add rune-scroller
# or
yarn add rune-scroller

šŸš€ Quick Start

Step 1: Import CSS (required)

āš ļø Important: You must import the CSS file once in your app.

Option A - In your root layout (recommended for SvelteKit):

<!-- src/routes/+layout.svelte -->
<script>
    import 'rune-scroller/animations.css';
</script>

<slot />

Option B - In each component that uses animations:

<script>
    import runeScroller from 'rune-scroller';
    import 'rune-scroller/animations.css';
</script>

Step 2: Use the animations

<script>
    import runeScroller from 'rune-scroller';
    // CSS already imported in layout or above
</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>

šŸŽØ Available Animations

Fade (5)

  • fade-in - Simple opacity fade
  • fade-in-up - Fade + move up 300px
  • fade-in-down - Fade + move down 300px
  • fade-in-left - Fade + move from right 300px
  • fade-in-right - Fade + move from left 300px

Zoom (5)

  • zoom-in - Scale from 0.3 to 1
  • zoom-out - Scale from 2 to 1
  • zoom-in-up - Zoom (0.5→1) + move up 300px
  • zoom-in-left - Zoom (0.5→1) + move from right 300px
  • zoom-in-right - Zoom (0.5→1) + move from left 300px

Others (4)

  • flip - 3D flip on Y-axis
  • flip-x - 3D flip on X-axis
  • slide-rotate - Slide + rotate 10°
  • bounce-in - Bouncy entrance (spring effect)

āš™ļø Options

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 = above)
}

Option Details

  • animation - Type of animation to play. Choose from 14 built-in animations listed above.
  • duration - How long the animation lasts in milliseconds (default: 2000ms).
  • repeat - If true, animation plays every time sentinel enters viewport. If false, plays only once.
  • debug - If true, displays the sentinel element as a visible cyan line below your element. Useful for seeing exactly when animations trigger.
  • offset - Offset of the sentinel in pixels. Positive values move sentinel down (delays animation), negative values move it up (triggers earlier). Useful for large elements where you want animation to trigger before the entire element is visible.

Examples

<!-- Basic -->
<div use:runeScroller={{ animation: 'zoom-in' }}>
    Content
</div>

<!-- Custom duration -->
<div use:runeScroller={{ animation: 'fade-in-up', duration: 1000 }}>
    Fast animation
</div>

<!-- Repeat mode -->
<div use:runeScroller={{ animation: 'bounce-in', repeat: true }}>
    Repeats every time you scroll
</div>

<!-- Debug mode - shows cyan line marking sentinel position -->
<div use:runeScroller={{ animation: 'fade-in', debug: true }}>
    The cyan line below this shows when animation will trigger
</div>

<!-- Multiple options -->
<div use:runeScroller={{
    animation: 'fade-in-up',
    duration: 1200,
    repeat: true,
    debug: true
}}>
    Full featured example
</div>

<!-- Large element - trigger animation earlier with negative offset -->
<div use:runeScroller={{
    animation: 'fade-in-up',
    offset: -200  // Trigger 200px before element bottom
}}>
    Large content that needs early triggering
</div>

<!-- Delay animation by moving sentinel down -->
<div use:runeScroller={{
    animation: 'zoom-in',
    offset: 300  // Trigger 300px after element bottom
}}>
    Content with delayed animation
</div>

šŸ”§ Advanced Usage

Using the animate Action (Direct Control)

For advanced use cases, use animate for fine-grained IntersectionObserver control:

<script>
    import { animate } from 'rune-scroller';
    import 'rune-scroller/animations.css';
</script>

<div use:animate={{
    animation: 'fade-in-up',
    duration: 1000,
    delay: 200,
    threshold: 0.5,
    offset: 20
}}>
    Advanced control
</div>

Options:

  • threshold - Intersection ratio to trigger (0-1, default: 0)
  • offset - Viewport offset percentage (0-100)
  • rootMargin - Custom IntersectionObserver margin
  • delay - Animation delay in ms (default: 0)

Note: animate triggers the animation once when the element enters the viewport (it's one-time by default, unlike runeScroller with repeat: true)

Using Composables

<script>
    import { useIntersectionOnce } from 'rune-scroller';
    import 'rune-scroller/animations.css';

    const intersection = useIntersectionOnce({ threshold: 0.5 });
</script>

<div
    bind:this={intersection.element}
    class="scroll-animate"
    class:is-visible={intersection.isVisible}
    data-animation="fade-in-up"
>
    Manual control over intersection state
</div>

šŸŽÆ How It Works

Rune Scroller uses sentinel-based triggering:

  1. An invisible 1px sentinel element is created below your element
  2. When the sentinel enters the viewport, animation triggers
  3. This ensures precise timing regardless of element size
  4. Uses native IntersectionObserver for performance
  5. Pure CSS animations (GPU-accelerated)

Why sentinels?

  • Accurate timing across all screen sizes
  • No complex offset calculations
  • Handles staggered animations naturally

🌐 SSR Compatibility

Works seamlessly with SvelteKit. Import CSS in your root layout:

<!-- src/routes/+layout.svelte -->
<script>
    import 'rune-scroller/animations.css';
</script>

<slot />

Then use animations anywhere in your app:

<!-- src/routes/+page.svelte -->
<script>
    import runeScroller from 'rune-scroller';
</script>

<!-- No special handling needed -->
<div use:runeScroller={{ animation: 'fade-in-up' }}>
    Works in SvelteKit SSR!
</div>

The library checks for browser environment and gracefully handles server-side rendering.


♿ Accessibility

Respects prefers-reduced-motion:

/* In animations.css */
@media (prefers-reduced-motion: reduce) {
    .scroll-animate {
        animation: none !important;
        opacity: 1 !important;
        transform: none !important;
    }
}

Users who prefer reduced motion will see content without animations.


šŸ“š API Reference

Public API

Rune Scroller exports 2 action-based APIs (no components):

  1. runeScroller (default) - Recommended, sentinel-based, simple
  2. animate (advanced) - Direct observation, fine-grained control

Why actions instead of components?

  • Actions are lightweight directives
  • No DOM wrapper overhead
  • Better performance
  • More flexible

Main Export

// Default export (recommended)
import runeScroller from 'rune-scroller';

// Named exports
import {
    animate,                    // Alternative action
    useIntersection,            // Composable
    useIntersectionOnce,        // Composable
    calculateRootMargin         // Utility
} from 'rune-scroller';

// Types
import type {
    AnimationType,
    RuneScrollerOptions,
    AnimateOptions,
    IntersectionOptions,
    UseIntersectionReturn
} from 'rune-scroller';

TypeScript Types

type AnimationType =
    | 'fade-in' | 'fade-in-up' | 'fade-in-down' | 'fade-in-left' | 'fade-in-right'
    | 'zoom-in' | 'zoom-out' | 'zoom-in-up' | 'zoom-in-left' | 'zoom-in-right'
    | 'flip' | 'flip-x' | 'slide-rotate' | 'bounce-in';

interface RuneScrollerOptions {
    animation?: AnimationType;
    duration?: number;
    repeat?: boolean;
    debug?: boolean;
    offset?: number;
}

interface AnimateOptions {
    animation?: AnimationType;
    duration?: number;
    delay?: number;
    threshold?: number;
    rootMargin?: string;
    offset?: number;
    once?: boolean;
}

šŸ“– Examples

Staggered Animations

<script>
    import runeScroller from 'rune-scroller';
    import 'rune-scroller/animations.css';

    const items = [
        { title: 'Feature 1', description: 'Description 1' },
        { title: 'Feature 2', description: 'Description 2' },
        { title: 'Feature 3', description: 'Description 3' }
    ];
</script>

<div class="grid">
    {#each items as item}
        <div use:runeScroller={{ animation: 'fade-in-up', duration: 800 }}>
            <h3>{item.title}</h3>
            <p>{item.description}</p>
        </div>
    {/each}
</div>

Hero Section

<div use:runeScroller={{ animation: 'fade-in-down', duration: 1000 }}>
    <h1>Welcome</h1>
</div>

<div use:runeScroller={{ animation: 'fade-in-up', duration: 1200 }}>
    <p>Engaging content</p>
</div>

<div use:runeScroller={{ animation: 'zoom-in', duration: 1000 }}>
    <button>Get Started</button>
</div>


šŸš€ SSR Compatibility

Full SSR Support - Rune Scroller is fully compatible with SvelteKit and server-side rendering:

How it works:

  • Actions (use:runeScroller and use:animate) only execute in the browser
  • Svelte automatically skips all actions during server-side rendering
  • No DOM errors or warnings during build/render
  • Content renders normally server-side (animations only apply in browser)

Result:

  • āœ… Zero SSR configuration needed
  • āœ… Animations gracefully skip on server, activate in browser
  • āœ… Perfect for SvelteKit projects with ssr: true

Example:

<!-- This works perfectly in SvelteKit with SSR enabled -->
<script>
    import runeScroller from 'rune-scroller';
    import 'rune-scroller/animations.css';
</script>

<div use:runeScroller={{ animation: 'fade-in' }}>
    <!-- Content renders on server, animates in browser -->
</div>

šŸ“„ License

MIT Ā© ludoloops


šŸ¤ Contributing

Contributions welcome! Please open an issue or PR on GitHub.

# Development
bun install
bun run dev
bun test
bun run build

Made with ā¤ļø by LeLab.dev

Top categories

Loading Svelte Themes