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
npm install rune-scroller
import 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.
<script>
import rs from 'rune-scroller';
</script>
<div use:rs={{ animation: 'fade-up' }}>Animates on scroll</div>
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>
</>
);
}
<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>
// 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>
<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>
data-aos attributes, same init() APItranslate3dprefers-reduced-motion| 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.
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 fadeszoom-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 + translateslide-up / slide-down / slide-left / slide-right ā Slide from off-screenflip-left / flip-right ā 3D flip on Y-axisflip-up / flip-down ā 3D flip on X-axisslide-rotate ā Slide + rotatebounce-in ā Bouncy spring entranceAll animations use the --rs-distance CSS variable (default: 100px):
<div data-aos="fade-up" style="--rs-distance: 200px">Farther slide</div>
| 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({
offset: 120,
duration: 400,
delay: 0,
easing: "ease",
once: false,
mirror: false,
startEvent: "DOMContentLoaded",
});
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;
}
translate3d)No wrapper divs ā the element itself becomes the positioning context. Your flex/grid layouts stay intact.
Respects prefers-reduced-motion ā animations are disabled automatically.
// 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";
<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}
<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>
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.
MIT Ā© ludoloops
Made with ā¤ļø by LeLab.dev