rune-scroller
v4.0.0
Published
Lightweight scroll animations for Svelte 5. Drop-in AOS replacement. 30 animations, zero dependencies.
Maintainers
Readme
⚡ Rune Scroller
Lightweight scroll animations. AOS replacement. Works everywhere.
Built with native IntersectionObserver — zero JS on scroll, GPU-accelerated, ~5.6KB gzipped.
🚀 Open Source by ludoloops at LeLab.dev 📜 Licensed under MIT
🚀 Quick Start
Any framework — Svelte, React, Vue, Angular, Vanilla JS
npm install rune-scrollerimport AOS from "rune-scroller/aos";
AOS.init();<div data-aos="fade-up" data-aos-duration="800">Animated</div>
<div data-aos="zoom-in" data-aos-delay="200">Delayed zoom</div>That's it. Same API as AOS. Works everywhere.
Svelte (native action)
<script>
import rs from 'rune-scroller';
</script>
<div use:rs={{ animation: 'fade-up' }}>Animates on scroll</div>React
import { useEffect } from "react";
### React (not tested — should work)
```jsx
import { useEffect } from 'react';
import AOS from 'rune-scroller/aos';
function App() {
useEffect(() => { AOS.init(); }, []);
return (
<>
<h1 data-aos="fade-down">Welcome</h1>
<p data-aos="fade-up" data-aos-delay="200">Subtitle</p>
</>
);
}Vue (not tested — should work)
<script setup>
import { onMounted } from "vue";
import AOS from "rune-scroller/aos";
onMounted(() => AOS.init());
</script>
<template>
<div data-aos="fade-up">Animated</div>
</template>Angular (not tested — should work)
// app.component.ts
import { Component, OnInit } from "@angular/core";
import AOS from "rune-scroller/aos";
@Component({ selector: "app-root", templateUrl: "./app.component.html" })
export class AppComponent implements OnInit {
ngOnInit() {
AOS.init();
}
}<!-- app.component.html -->
<div data-aos="fade-up">Animated</div>CDN (not tested — should work)
<script type="module">
import AOS from "https://esm.sh/rune-scroller/aos";
AOS.init();
</script>
<div data-aos="fade-up">Works without any build step</div>✨ Features
- Framework agnostic — Svelte, React, Vue, Angular, Vanilla JS, CDN
- AOS drop-in — Same
data-aosattributes, sameinit()API - Zero dependencies — Pure JS + native IntersectionObserver
- ~5.6KB gzipped — Smaller than AOS (6.9KB)
- 37 animations — Fade, Zoom, Flip, Slide, Bounce
- Zero JS on scroll — Browser handles detection natively
- TypeScript support — Full type definitions
- SSR-ready — SvelteKit, Next.js, Nuxt compatible
- GPU-accelerated — CSS transforms via
translate3d - Accessible — Respects
prefers-reduced-motion - No wrapper divs — Your layouts stay intact
AOS vs rune-scroller
| | rune-scroller | AOS |
| ------------------------- | ---------------------------------------------- | ------------------------------------------ |
| Bundle size (gzipped) | ~5.6KB JS+CSS | ~6.9KB JS+CSS |
| Dependencies | 0 | lodash.throttle, lodash.debounce |
| Scroll detection | IntersectionObserver (native, C++) | Scroll event + throttle (JS) |
| Per-scroll cost | 0 — browser handles it | Iterates ALL elements every 99ms |
| Layout reads | 1 per element (init only) | offsetParent loop per element per scroll |
| Resize handling | ResizeObserver (native) | debounced scroll recalc |
| 100 animated elements | ~0ms per scroll | ~2-5ms per scroll (layout thrashing) |
| Animations | 30 | 28 |
| Framework | Any (Svelte, React, Vue, Angular, Vanilla) | Vanilla JS only |
The key difference: AOS runs JavaScript on every scroll event for every element. rune-scroller delegates detection to the browser's native IntersectionObserver — zero JS execution until an element actually enters the viewport.
🎨 Available Animations (30)
Fade (10)
fade— Simple opacity fadefade-up/fade-down/fade-left/fade-right— Fade + translatefade-up-right/fade-up-left/fade-down-right/fade-down-left— Diagonal fades
Zoom (10)
zoom-in/zoom-out— Scale in/outzoom-in-up/zoom-in-down/zoom-in-left/zoom-in-right— Zoom + translatezoom-out-up/zoom-out-down/zoom-out-left/zoom-out-right— Zoom out + translate
Slide (4)
slide-up/slide-down/slide-left/slide-right— Slide from off-screen
Flip (4)
flip-left/flip-right— 3D flip on Y-axisflip-up/flip-down— 3D flip on X-axis
Special (2)
slide-rotate— Slide + rotatebounce-in— Bouncy spring entrance
Customizable distance
All animations use the --rs-distance CSS variable (default: 100px):
<div data-aos="fade-up" style="--rs-distance: 200px">Farther slide</div>⚙️ Options
AOS Mode (data attributes)
| Attribute | Example | Description |
| ------------------- | --------------- | -------------------------- |
| data-aos | "fade-up" | Animation name |
| data-aos-duration | "800" | Duration in ms |
| data-aos-delay | "200" | Delay in ms |
| data-aos-easing | "ease-in-out" | CSS timing function |
| data-aos-offset | "120" | Trigger offset in px |
| data-aos-once | "true" | Animate only once |
| data-aos-mirror | "true" | Animate on scroll away too |
AOS init options
AOS.init({
offset: 120,
duration: 400,
delay: 0,
easing: "ease",
once: false,
mirror: false,
startEvent: "DOMContentLoaded",
});Svelte Action options
interface RuneScrollerOptions {
animation?: AnimationType; // default: 'fade-up'
duration?: number; // default: 400
delay?: number; // default: 0
easing?: string; // default: 'ease'
repeat?: boolean; // default: false
debug?: boolean;
offset?: number; // negative = earlier trigger
onVisible?: (el: HTMLElement) => void;
sentinelColor?: string;
sentinelId?: string;
}🎯 How It Works
- Invisible 1px sentinel appended as child of the animated element
- When sentinel enters viewport, animation triggers via IntersectionObserver
- Pure CSS transitions (GPU-accelerated via
translate3d) - ResizeObserver auto-repositions sentinel
No wrapper divs — the element itself becomes the positioning context. Your flex/grid layouts stay intact.
♿ Accessibility
Respects prefers-reduced-motion — animations are disabled automatically.
📚 API Reference
// Framework agnostic (AOS mode)
import AOS from "rune-scroller/aos";
AOS.init();
AOS.refresh();
AOS.refreshHard();
// Svelte action (default)
import rs from "rune-scroller";
// Named exports
import {
runeScroller,
useIntersection,
useIntersectionOnce,
calculateRootMargin,
ANIMATION_TYPES,
} from "rune-scroller";
// Types
import type { AnimationType, RuneScrollerOptions } from "rune-scroller";📖 Examples
Staggered Animations
<script>
import rs from 'rune-scroller';
const items = ['Item 1', 'Item 2', 'Item 3'];
</script>
{#each items as item, i}
<div use:rs={{ animation: 'fade-up', duration: 800, delay: i * 100 }}>
{item}
</div>
{/each}Hero Section
<h1 data-aos="fade-down" data-aos-duration="1000">Welcome</h1>
<p data-aos="fade-up" data-aos-duration="1200">Subtitle</p>
<button data-aos="zoom-in" data-aos-duration="800">Get Started</button>🔄 Replacing AOS
npm uninstall aos
npm install rune-scroller- import AOS from 'aos';
- import 'aos/dist/aos.css';
+ import AOS from 'rune-scroller/aos';Everything else stays the same. Same attributes, same options.
🔗 Links
- npm: rune-scroller
- GitHub: lelabdev/rune-scroller
- Changelog: CHANGELOG.md
📄 License
MIT © ludoloops
Made with ❤️ by LeLab.dev
