rune-scroller 0.1.9 → 0.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +196 -581
- package/package.json +14 -15
package/README.md
CHANGED
|
@@ -4,109 +4,48 @@
|
|
|
4
4
|
<img src="./logo.png" alt="Rune Scroller Logo" width="200" />
|
|
5
5
|
</div>
|
|
6
6
|
|
|
7
|
-
**
|
|
7
|
+
**Lightweight scroll animations for Svelte 5** — Built with Svelte 5 Runes and IntersectionObserver API.
|
|
8
8
|
|
|
9
|
-
> 🚀 **Open Source
|
|
10
|
-
> 📜 Licensed under **MIT**
|
|
11
|
-
>
|
|
12
|
-
> A modern, lightweight scroll animation library showcasing Svelte 5 capabilities
|
|
9
|
+
> 🚀 **Open Source** by [ludoloops](https://github.com/ludoloops) at [LeLab.dev](https://lelab.dev)
|
|
10
|
+
> 📜 Licensed under **MIT**
|
|
13
11
|
|
|
14
12
|
---
|
|
15
13
|
|
|
16
14
|
## ✨ Features
|
|
17
15
|
|
|
18
|
-
-
|
|
19
|
-
-
|
|
20
|
-
-
|
|
21
|
-
-
|
|
22
|
-
-
|
|
23
|
-
-
|
|
24
|
-
-
|
|
25
|
-
- ✅ **Play Once or Repeat** : Control animation behavior
|
|
26
|
-
- ✅ **SSR-ready** : SvelteKit compatible with no DOM access during hydration
|
|
27
|
-
- ✅ **Accessible** : Respects `prefers-reduced-motion` media query
|
|
16
|
+
- **~2KB gzipped** - Minimal overhead
|
|
17
|
+
- **Zero dependencies** - Pure Svelte 5 + IntersectionObserver
|
|
18
|
+
- **14 animations** - Fade, Zoom, Flip, Slide, Bounce variants
|
|
19
|
+
- **TypeScript** - Full type coverage
|
|
20
|
+
- **SSR-ready** - SvelteKit compatible
|
|
21
|
+
- **GPU-accelerated** - Pure CSS transforms
|
|
22
|
+
- **Accessible** - Respects `prefers-reduced-motion`
|
|
28
23
|
|
|
29
24
|
---
|
|
30
25
|
|
|
31
|
-
##
|
|
32
|
-
|
|
33
|
-
### When Using in a Svelte Project
|
|
34
|
-
|
|
35
|
-
| Scenario | Bundle Size | Impact |
|
|
36
|
-
| ------------------------- | ----------------- | -------------------- |
|
|
37
|
-
| **Svelte App (baseline)** | ~30-35 KB gzipped | - |
|
|
38
|
-
| **+ AOS Library** | ~34-39 KB | **+4 KB overhead** |
|
|
39
|
-
| **+ Rune Scroller** | ~31.9-36.9 KB | **+1.9 KB overhead** |
|
|
40
|
-
| **Savings** | **2.1 KB** | **52% smaller** ✨ |
|
|
41
|
-
|
|
42
|
-
### Why Rune Scroller is Lighter
|
|
43
|
-
|
|
44
|
-
1. **Native Svelte Integration** - Uses `$state()` directly (no separate state lib)
|
|
45
|
-
2. **CSS-Based Animations** - Pure CSS transforms + GPU acceleration (no JS animation loop)
|
|
46
|
-
3. **Svelte 5 Optimized** - Leverages runes system for minimal overhead
|
|
47
|
-
4. **Zero External Dependencies** - Works with Svelte's native IntersectionObserver
|
|
48
|
-
|
|
49
|
-
### Real-World Impact
|
|
50
|
-
|
|
51
|
-
For a typical SvelteKit app:
|
|
52
|
-
|
|
53
|
-
- **With AOS**: Extra 4 KB per user download
|
|
54
|
-
- **With Rune Scroller**: Extra 1.9 KB per user download
|
|
55
|
-
- **Difference**: Save **2.1 KB per page load** = faster initial paint! 🚀
|
|
56
|
-
|
|
57
|
-
---
|
|
58
|
-
|
|
59
|
-
## 📦 Project Structure
|
|
60
|
-
|
|
61
|
-
```
|
|
62
|
-
rune-scroller-lib/
|
|
63
|
-
├── src/lib/
|
|
64
|
-
│ ├── runeScroller.svelte.ts # Main animation action (sentinel-based)
|
|
65
|
-
│ ├── useIntersection.svelte.ts # IntersectionObserver composables
|
|
66
|
-
│ ├── animate.svelte.ts # Animation action for direct DOM control
|
|
67
|
-
│ ├── animations.ts # Animation configuration & validation
|
|
68
|
-
│ ├── animations.css # Animation styles (14 animations)
|
|
69
|
-
│ ├── animations.test.ts # Animation configuration tests
|
|
70
|
-
│ ├── scroll-animate.test.ts # Component behavior tests
|
|
71
|
-
│ └── index.ts # Library entry point
|
|
72
|
-
├── dist/ # Built library (created by pnpm build)
|
|
73
|
-
├── package.json # npm package configuration
|
|
74
|
-
├── svelte.config.js # SvelteKit configuration
|
|
75
|
-
├── vite.config.ts # Vite build configuration
|
|
76
|
-
├── tsconfig.json # TypeScript configuration
|
|
77
|
-
└── eslint.config.js # ESLint configuration
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
---
|
|
81
|
-
|
|
82
|
-
## 🚀 Quick Start
|
|
83
|
-
|
|
84
|
-
### Installation
|
|
26
|
+
## 📦 Installation
|
|
85
27
|
|
|
86
28
|
```bash
|
|
87
29
|
npm install rune-scroller
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
Or with other package managers:
|
|
91
|
-
|
|
92
|
-
```bash
|
|
30
|
+
# or
|
|
93
31
|
pnpm add rune-scroller
|
|
32
|
+
# or
|
|
94
33
|
yarn add rune-scroller
|
|
95
34
|
```
|
|
96
35
|
|
|
97
|
-
|
|
36
|
+
---
|
|
98
37
|
|
|
99
|
-
|
|
38
|
+
## 🚀 Quick Start
|
|
100
39
|
|
|
101
40
|
```svelte
|
|
102
41
|
<script>
|
|
103
42
|
import runeScroller from 'rune-scroller';
|
|
43
|
+
import 'rune-scroller/animations.css';
|
|
104
44
|
</script>
|
|
105
45
|
|
|
106
|
-
<!-- Simple
|
|
46
|
+
<!-- Simple animation -->
|
|
107
47
|
<div use:runeScroller={{ animation: 'fade-in' }}>
|
|
108
48
|
<h2>Animated Heading</h2>
|
|
109
|
-
<p>Animates when scrolled into view</p>
|
|
110
49
|
</div>
|
|
111
50
|
|
|
112
51
|
<!-- With custom duration -->
|
|
@@ -114,422 +53,256 @@ Use the `runeScroller` action with the `use:` directive for sentinel-based anima
|
|
|
114
53
|
<div class="card">Smooth fade and slide</div>
|
|
115
54
|
</div>
|
|
116
55
|
|
|
117
|
-
<!-- Repeat
|
|
118
|
-
<div use:runeScroller={{ animation: 'bounce-in',
|
|
56
|
+
<!-- Repeat on every scroll -->
|
|
57
|
+
<div use:runeScroller={{ animation: 'bounce-in', repeat: true }}>
|
|
119
58
|
<button>Bounces on every scroll</button>
|
|
120
59
|
</div>
|
|
121
60
|
```
|
|
122
61
|
|
|
123
|
-
### How It Works
|
|
124
|
-
|
|
125
|
-
The `runeScroller` action uses an invisible **sentinel element** for precise animation triggering:
|
|
126
|
-
|
|
127
|
-
1. **Sentinel Creation** - An invisible sentinel is created and positioned **absolutely** at your element's bottom
|
|
128
|
-
2. **Parent Context** - Parent element automatically gets `position: relative` if needed (no visual impact)
|
|
129
|
-
3. **Viewport Detection** - When the sentinel enters the viewport, it triggers the animation
|
|
130
|
-
4. **Fixed Positioning** - The sentinel stays fixed while your element animates, ensuring accurate timing
|
|
131
|
-
5. **Invisible by Default** - The sentinel is 1px tall and invisible (`visibility:hidden`)
|
|
132
|
-
6. **Debug Mode** - Add `debug: true` to see the sentinel as a visible cyan line (useful for development)
|
|
133
|
-
|
|
134
62
|
---
|
|
135
63
|
|
|
136
|
-
##
|
|
137
|
-
|
|
138
|
-
### Why Sentinels?
|
|
139
|
-
|
|
140
|
-
- **Accurate Timing** - Triggers at the perfect moment for smooth animations
|
|
141
|
-
- **Consistent Behavior** - Same timing across all screen sizes and content heights
|
|
142
|
-
- **Simple API** - No complex offset calculations needed
|
|
143
|
-
- **Performance** - Minimal overhead, pure CSS animations
|
|
144
|
-
- **Reliable** - Automatic sentinel placement handles edge cases
|
|
145
|
-
|
|
146
|
-
### Sentinel-Based Examples
|
|
147
|
-
|
|
148
|
-
**Staggered animations with sentinels:**
|
|
149
|
-
|
|
150
|
-
```svelte
|
|
151
|
-
<script>
|
|
152
|
-
import runeScroller from 'rune-scroller';
|
|
153
|
-
</script>
|
|
64
|
+
## 🎨 Available Animations
|
|
154
65
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
{/each}
|
|
162
|
-
</div>
|
|
163
|
-
```
|
|
66
|
+
### Fade (5)
|
|
67
|
+
- `fade-in` - Simple opacity fade
|
|
68
|
+
- `fade-in-up` - Fade + move up 100px
|
|
69
|
+
- `fade-in-down` - Fade + move down 100px
|
|
70
|
+
- `fade-in-left` - Fade + move from right
|
|
71
|
+
- `fade-in-right` - Fade + move from left
|
|
164
72
|
|
|
165
|
-
|
|
73
|
+
### Zoom (5)
|
|
74
|
+
- `zoom-in` - Scale from 0.6 to 1
|
|
75
|
+
- `zoom-out` - Scale from 1.2 to 1
|
|
76
|
+
- `zoom-in-up` - Zoom + move up
|
|
77
|
+
- `zoom-in-left` - Zoom + move from right
|
|
78
|
+
- `zoom-in-right` - Zoom + move from left
|
|
166
79
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
80
|
+
### Others (4)
|
|
81
|
+
- `flip` - 3D flip on Y-axis
|
|
82
|
+
- `flip-x` - 3D flip on X-axis
|
|
83
|
+
- `slide-rotate` - Slide + rotate 10°
|
|
84
|
+
- `bounce-in` - Bouncy entrance (spring effect)
|
|
171
85
|
|
|
172
|
-
|
|
173
|
-
<p>Engaging content appears as you scroll</p>
|
|
174
|
-
</div>
|
|
175
|
-
|
|
176
|
-
<div use:runeScroller={{ animation: 'zoom-in', duration: 1000 }}>
|
|
177
|
-
<button class="cta">Get Started</button>
|
|
178
|
-
</div>
|
|
179
|
-
```
|
|
86
|
+
---
|
|
180
87
|
|
|
181
|
-
|
|
88
|
+
## ⚙️ Options
|
|
182
89
|
|
|
183
90
|
```typescript
|
|
184
91
|
interface RuneScrollerOptions {
|
|
185
|
-
animation?: AnimationType;
|
|
186
|
-
duration?: number;
|
|
187
|
-
repeat?: boolean;
|
|
92
|
+
animation?: AnimationType; // Animation name (default: 'fade-in')
|
|
93
|
+
duration?: number; // Duration in ms (default: 2000)
|
|
94
|
+
repeat?: boolean; // Repeat on scroll (default: false)
|
|
95
|
+
debug?: boolean; // Show sentinel element (default: false)
|
|
188
96
|
}
|
|
189
97
|
```
|
|
190
98
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
## 🎨 All Animations with Examples
|
|
194
|
-
|
|
195
|
-
### Fade (5 variants)
|
|
196
|
-
|
|
197
|
-
#### `fade-in`
|
|
198
|
-
|
|
199
|
-
Simple opacity fade from transparent to visible.
|
|
200
|
-
|
|
201
|
-
```svelte
|
|
202
|
-
<script>
|
|
203
|
-
import runeScroller from 'rune-scroller';
|
|
204
|
-
</script>
|
|
205
|
-
|
|
206
|
-
<div use:runeScroller={{ animation: 'fade-in' }}>
|
|
207
|
-
<h2>Fade In</h2>
|
|
208
|
-
<p>Simple fade entrance</p>
|
|
209
|
-
</div>
|
|
210
|
-
```
|
|
211
|
-
|
|
212
|
-
#### `fade-in-up`
|
|
213
|
-
|
|
214
|
-
Fades in while moving up 100px.
|
|
215
|
-
|
|
216
|
-
```svelte
|
|
217
|
-
<div use:runeScroller={{ animation: 'fade-in-up' }}>
|
|
218
|
-
<h2>Fade In Up</h2>
|
|
219
|
-
<p>Rises from below</p>
|
|
220
|
-
</div>
|
|
221
|
-
```
|
|
222
|
-
|
|
223
|
-
#### `fade-in-down`
|
|
224
|
-
|
|
225
|
-
Fades in while moving down 100px.
|
|
226
|
-
|
|
227
|
-
```svelte
|
|
228
|
-
<div use:runeScroller={{ animation: 'fade-in-down' }}>
|
|
229
|
-
<h2>Fade In Down</h2>
|
|
230
|
-
<p>Descends from above</p>
|
|
231
|
-
</div>
|
|
232
|
-
```
|
|
233
|
-
|
|
234
|
-
#### `fade-in-left`
|
|
235
|
-
|
|
236
|
-
Fades in while moving left 100px.
|
|
237
|
-
|
|
238
|
-
```svelte
|
|
239
|
-
<div use:runeScroller={{ animation: 'fade-in-left' }}>
|
|
240
|
-
<h2>Fade In Left</h2>
|
|
241
|
-
<p>Comes from the right</p>
|
|
242
|
-
</div>
|
|
243
|
-
```
|
|
244
|
-
|
|
245
|
-
#### `fade-in-right`
|
|
246
|
-
|
|
247
|
-
Fades in while moving right 100px.
|
|
248
|
-
|
|
249
|
-
```svelte
|
|
250
|
-
<div use:runeScroller={{ animation: 'fade-in-right' }}>
|
|
251
|
-
<h2>Fade In Right</h2>
|
|
252
|
-
<p>Comes from the left</p>
|
|
253
|
-
</div>
|
|
254
|
-
```
|
|
255
|
-
|
|
256
|
-
---
|
|
257
|
-
|
|
258
|
-
### Zoom (5 variants)
|
|
259
|
-
|
|
260
|
-
#### `zoom-in`
|
|
261
|
-
|
|
262
|
-
Scales from 50% to 100% while fading in.
|
|
99
|
+
### Examples
|
|
263
100
|
|
|
264
101
|
```svelte
|
|
102
|
+
<!-- Basic -->
|
|
265
103
|
<div use:runeScroller={{ animation: 'zoom-in' }}>
|
|
266
|
-
|
|
267
|
-
<p>Grows into view</p>
|
|
104
|
+
Content
|
|
268
105
|
</div>
|
|
269
|
-
```
|
|
270
|
-
|
|
271
|
-
#### `zoom-out`
|
|
272
|
-
|
|
273
|
-
Scales from 150% to 100% while fading in.
|
|
274
106
|
|
|
275
|
-
|
|
276
|
-
<div use:runeScroller={{ animation: '
|
|
277
|
-
|
|
278
|
-
<p>Shrinks into view</p>
|
|
107
|
+
<!-- Custom duration -->
|
|
108
|
+
<div use:runeScroller={{ animation: 'fade-in-up', duration: 1000 }}>
|
|
109
|
+
Fast animation
|
|
279
110
|
</div>
|
|
280
|
-
```
|
|
281
|
-
|
|
282
|
-
#### `zoom-in-up`
|
|
283
111
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
<div use:runeScroller={{ animation: 'zoom-in-up' }}>
|
|
288
|
-
<h2>Zoom In Up</h2>
|
|
289
|
-
<p>Grows while moving up</p>
|
|
112
|
+
<!-- Repeat mode -->
|
|
113
|
+
<div use:runeScroller={{ animation: 'bounce-in', repeat: true }}>
|
|
114
|
+
Repeats every time you scroll
|
|
290
115
|
</div>
|
|
291
|
-
```
|
|
292
|
-
|
|
293
|
-
#### `zoom-in-left`
|
|
294
116
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
<div use:runeScroller={{ animation: 'zoom-in-left' }}>
|
|
299
|
-
<h2>Zoom In Left</h2>
|
|
300
|
-
<p>Grows while moving left</p>
|
|
301
|
-
</div>
|
|
302
|
-
```
|
|
303
|
-
|
|
304
|
-
#### `zoom-in-right`
|
|
305
|
-
|
|
306
|
-
Scales from 50% while translating right 50px.
|
|
307
|
-
|
|
308
|
-
```svelte
|
|
309
|
-
<div use:runeScroller={{ animation: 'zoom-in-right' }}>
|
|
310
|
-
<h2>Zoom In Right</h2>
|
|
311
|
-
<p>Grows while moving right</p>
|
|
117
|
+
<!-- Debug mode (shows invisible sentinel) -->
|
|
118
|
+
<div use:runeScroller={{ animation: 'fade-in', debug: true }}>
|
|
119
|
+
You'll see a cyan line (the sentinel trigger)
|
|
312
120
|
</div>
|
|
313
121
|
```
|
|
314
122
|
|
|
315
123
|
---
|
|
316
124
|
|
|
317
|
-
|
|
125
|
+
## 🔧 Advanced Usage
|
|
318
126
|
|
|
319
|
-
|
|
127
|
+
### Using the `animate` Action (Direct Control)
|
|
320
128
|
|
|
321
|
-
|
|
129
|
+
For advanced use cases, use `animate` for fine-grained IntersectionObserver control:
|
|
322
130
|
|
|
323
131
|
```svelte
|
|
324
|
-
<
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
</
|
|
328
|
-
```
|
|
329
|
-
|
|
330
|
-
#### `flip-x`
|
|
331
|
-
|
|
332
|
-
3D rotation on X axis (top to bottom).
|
|
132
|
+
<script>
|
|
133
|
+
import { animate } from 'rune-scroller';
|
|
134
|
+
import 'rune-scroller/animations.css';
|
|
135
|
+
</script>
|
|
333
136
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
137
|
+
<div use:animate={{
|
|
138
|
+
animation: 'fade-in-up',
|
|
139
|
+
duration: 1000,
|
|
140
|
+
delay: 200,
|
|
141
|
+
threshold: 0.5,
|
|
142
|
+
offset: 20,
|
|
143
|
+
once: true
|
|
144
|
+
}}>
|
|
145
|
+
Advanced control
|
|
338
146
|
</div>
|
|
339
147
|
```
|
|
340
148
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
149
|
+
**Options:**
|
|
150
|
+
- `threshold` - Intersection ratio to trigger (0-1)
|
|
151
|
+
- `offset` - Viewport offset percentage (0-100)
|
|
152
|
+
- `rootMargin` - Custom IntersectionObserver margin
|
|
153
|
+
- `delay` - Animation delay in ms
|
|
154
|
+
- `once` - Trigger only once
|
|
344
155
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
Slides from left while rotating 45 degrees.
|
|
156
|
+
### Using Composables
|
|
348
157
|
|
|
349
158
|
```svelte
|
|
350
|
-
<
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
</div>
|
|
354
|
-
```
|
|
355
|
-
|
|
356
|
-
---
|
|
357
|
-
|
|
358
|
-
### Bounce
|
|
359
|
-
|
|
360
|
-
#### `bounce-in`
|
|
159
|
+
<script>
|
|
160
|
+
import { useIntersectionOnce } from 'rune-scroller';
|
|
161
|
+
import 'rune-scroller/animations.css';
|
|
361
162
|
|
|
362
|
-
|
|
163
|
+
const intersection = useIntersectionOnce({ threshold: 0.5 });
|
|
164
|
+
</script>
|
|
363
165
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
166
|
+
<div
|
|
167
|
+
bind:this={intersection.element}
|
|
168
|
+
class="scroll-animate"
|
|
169
|
+
class:is-visible={intersection.isVisible}
|
|
170
|
+
data-animation="fade-in-up"
|
|
171
|
+
>
|
|
172
|
+
Manual control over intersection state
|
|
368
173
|
</div>
|
|
369
174
|
```
|
|
370
175
|
|
|
371
176
|
---
|
|
372
177
|
|
|
373
|
-
|
|
178
|
+
## 🎯 How It Works
|
|
374
179
|
|
|
375
|
-
|
|
180
|
+
Rune Scroller uses **sentinel-based triggering**:
|
|
376
181
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
182
|
+
1. An invisible 1px sentinel element is created below your element
|
|
183
|
+
2. When the sentinel enters the viewport, animation triggers
|
|
184
|
+
3. This ensures precise timing regardless of element size
|
|
185
|
+
4. Uses native IntersectionObserver for performance
|
|
186
|
+
5. Pure CSS animations (GPU-accelerated)
|
|
382
187
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
```
|
|
188
|
+
**Why sentinels?**
|
|
189
|
+
- Accurate timing across all screen sizes
|
|
190
|
+
- No complex offset calculations
|
|
191
|
+
- Handles staggered animations naturally
|
|
388
192
|
|
|
389
193
|
---
|
|
390
194
|
|
|
391
|
-
##
|
|
392
|
-
|
|
393
|
-
### Staggered Grid
|
|
195
|
+
## 🌐 SSR Compatibility
|
|
394
196
|
|
|
395
|
-
|
|
197
|
+
Works seamlessly with SvelteKit:
|
|
396
198
|
|
|
397
199
|
```svelte
|
|
398
200
|
<script>
|
|
399
201
|
import runeScroller from 'rune-scroller';
|
|
202
|
+
import 'rune-scroller/animations.css';
|
|
400
203
|
</script>
|
|
401
204
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
<div class="card">
|
|
406
|
-
<h3>{item.title}</h3>
|
|
407
|
-
<p>{item.description}</p>
|
|
408
|
-
</div>
|
|
409
|
-
</div>
|
|
410
|
-
{/each}
|
|
205
|
+
<!-- No special handling needed -->
|
|
206
|
+
<div use:runeScroller={{ animation: 'fade-in-up' }}>
|
|
207
|
+
Works in SvelteKit SSR!
|
|
411
208
|
</div>
|
|
412
209
|
```
|
|
413
210
|
|
|
414
|
-
|
|
211
|
+
The library checks for browser environment and gracefully handles server-side rendering.
|
|
415
212
|
|
|
416
|
-
|
|
417
|
-
<script>
|
|
418
|
-
import runeScroller from 'rune-scroller';
|
|
419
|
-
</script>
|
|
213
|
+
---
|
|
420
214
|
|
|
421
|
-
|
|
422
|
-
Content fades in
|
|
423
|
-
</section>
|
|
215
|
+
## ♿ Accessibility
|
|
424
216
|
|
|
425
|
-
|
|
426
|
-
Content slides and rotates
|
|
427
|
-
</section>
|
|
217
|
+
Respects `prefers-reduced-motion`:
|
|
428
218
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
219
|
+
```css
|
|
220
|
+
/* In animations.css */
|
|
221
|
+
@media (prefers-reduced-motion: reduce) {
|
|
222
|
+
.scroll-animate {
|
|
223
|
+
animation: none !important;
|
|
224
|
+
opacity: 1 !important;
|
|
225
|
+
transform: none !important;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
432
228
|
```
|
|
433
229
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
```svelte
|
|
437
|
-
<script>
|
|
438
|
-
import runeScroller from 'rune-scroller';
|
|
439
|
-
</script>
|
|
440
|
-
|
|
441
|
-
<section class="hero">
|
|
442
|
-
<h1 use:runeScroller={{ animation: 'fade-in', duration: 1000 }}>
|
|
443
|
-
Welcome
|
|
444
|
-
</h1>
|
|
445
|
-
|
|
446
|
-
<p use:runeScroller={{ animation: 'fade-in', duration: 1200 }}>
|
|
447
|
-
Scroll to reveal more
|
|
448
|
-
</p>
|
|
449
|
-
|
|
450
|
-
<button use:runeScroller={{ animation: 'zoom-in', duration: 1000 }}>
|
|
451
|
-
Get Started
|
|
452
|
-
</button>
|
|
453
|
-
</section>
|
|
454
|
-
```
|
|
230
|
+
Users who prefer reduced motion will see content without animations.
|
|
455
231
|
|
|
456
232
|
---
|
|
457
233
|
|
|
458
|
-
##
|
|
459
|
-
|
|
460
|
-
### runeScroller (Recommended)
|
|
234
|
+
## 📚 API Reference
|
|
461
235
|
|
|
462
|
-
|
|
236
|
+
### Main Export
|
|
463
237
|
|
|
464
238
|
```typescript
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
239
|
+
// Default export (recommended)
|
|
240
|
+
import runeScroller from 'rune-scroller';
|
|
241
|
+
|
|
242
|
+
// Named exports
|
|
243
|
+
import {
|
|
244
|
+
animate, // Alternative action
|
|
245
|
+
useIntersection, // Composable
|
|
246
|
+
useIntersectionOnce, // Composable
|
|
247
|
+
calculateRootMargin // Utility
|
|
248
|
+
} from 'rune-scroller';
|
|
249
|
+
|
|
250
|
+
// Types
|
|
251
|
+
import type {
|
|
252
|
+
AnimationType,
|
|
253
|
+
RuneScrollerOptions,
|
|
254
|
+
AnimateOptions,
|
|
255
|
+
IntersectionOptions,
|
|
256
|
+
UseIntersectionReturn
|
|
257
|
+
} from 'rune-scroller';
|
|
473
258
|
```
|
|
474
259
|
|
|
475
|
-
|
|
476
|
-
- Automatically creates an invisible 20px sentinel element below your content
|
|
477
|
-
- Triggers animation when sentinel enters viewport
|
|
478
|
-
- Provides consistent timing across all screen sizes
|
|
479
|
-
- Minimal configuration needed
|
|
480
|
-
- Supports both one-time and repeating animations
|
|
260
|
+
### TypeScript Types
|
|
481
261
|
|
|
482
|
-
|
|
262
|
+
```typescript
|
|
263
|
+
type AnimationType =
|
|
264
|
+
| 'fade-in' | 'fade-in-up' | 'fade-in-down' | 'fade-in-left' | 'fade-in-right'
|
|
265
|
+
| 'zoom-in' | 'zoom-out' | 'zoom-in-up' | 'zoom-in-left' | 'zoom-in-right'
|
|
266
|
+
| 'flip' | 'flip-x' | 'slide-rotate' | 'bounce-in';
|
|
483
267
|
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
268
|
+
interface RuneScrollerOptions {
|
|
269
|
+
animation?: AnimationType;
|
|
270
|
+
duration?: number;
|
|
271
|
+
repeat?: boolean;
|
|
272
|
+
debug?: boolean;
|
|
273
|
+
}
|
|
488
274
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
275
|
+
interface AnimateOptions {
|
|
276
|
+
animation?: AnimationType;
|
|
277
|
+
duration?: number;
|
|
278
|
+
delay?: number;
|
|
279
|
+
threshold?: number;
|
|
280
|
+
rootMargin?: string;
|
|
281
|
+
offset?: number;
|
|
282
|
+
once?: boolean;
|
|
283
|
+
}
|
|
493
284
|
```
|
|
494
285
|
|
|
495
|
-
|
|
286
|
+
---
|
|
496
287
|
|
|
497
|
-
|
|
498
|
-
<!-- Animation repeats each time sentinel enters viewport -->
|
|
499
|
-
<div use:runeScroller={{ animation: 'bounce-in', duration: 800, repeat: true }}>
|
|
500
|
-
This animates every time you scroll past it
|
|
501
|
-
</div>
|
|
502
|
-
```
|
|
288
|
+
## 📖 Examples
|
|
503
289
|
|
|
504
|
-
|
|
290
|
+
### Staggered Animations
|
|
505
291
|
|
|
506
292
|
```svelte
|
|
507
293
|
<script>
|
|
508
294
|
import runeScroller from 'rune-scroller';
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
<!-- Fade in once on scroll -->
|
|
512
|
-
<div use:runeScroller={{ animation: 'fade-in', duration: 600 }}>
|
|
513
|
-
<h2>Section Title</h2>
|
|
514
|
-
<p>Fades in when scrolled into view</p>
|
|
515
|
-
</div>
|
|
516
|
-
|
|
517
|
-
<!-- Zoom in with longer duration -->
|
|
518
|
-
<div use:runeScroller={{ animation: 'zoom-in-up', duration: 1200 }}>
|
|
519
|
-
<div class="card">
|
|
520
|
-
<h3>Card Title</h3>
|
|
521
|
-
<p>Zooms in from below</p>
|
|
522
|
-
</div>
|
|
523
|
-
</div>
|
|
295
|
+
import 'rune-scroller/animations.css';
|
|
524
296
|
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
297
|
+
const items = [
|
|
298
|
+
{ title: 'Feature 1', description: 'Description 1' },
|
|
299
|
+
{ title: 'Feature 2', description: 'Description 2' },
|
|
300
|
+
{ title: 'Feature 3', description: 'Description 3' }
|
|
301
|
+
];
|
|
302
|
+
</script>
|
|
529
303
|
|
|
530
|
-
<!-- Complex staggered layout -->
|
|
531
304
|
<div class="grid">
|
|
532
|
-
{#each items as item
|
|
305
|
+
{#each items as item}
|
|
533
306
|
<div use:runeScroller={{ animation: 'fade-in-up', duration: 800 }}>
|
|
534
307
|
<h3>{item.title}</h3>
|
|
535
308
|
<p>{item.description}</p>
|
|
@@ -538,209 +311,51 @@ function runeScroller(
|
|
|
538
311
|
</div>
|
|
539
312
|
```
|
|
540
313
|
|
|
541
|
-
|
|
542
|
-
- ✅ Simple element animations
|
|
543
|
-
- ✅ Consistent timing across layouts
|
|
544
|
-
- ✅ Minimal overhead applications
|
|
545
|
-
- ✅ Both one-time and repeating animations
|
|
546
|
-
|
|
547
|
-
---
|
|
548
|
-
|
|
549
|
-
### useIntersectionOnce
|
|
550
|
-
|
|
551
|
-
For one-time animations:
|
|
552
|
-
|
|
553
|
-
```typescript
|
|
554
|
-
function useIntersectionOnce(options?: {
|
|
555
|
-
threshold?: number;
|
|
556
|
-
rootMargin?: string;
|
|
557
|
-
root?: Element | null;
|
|
558
|
-
}): { element: HTMLElement | null; isVisible: boolean }
|
|
559
|
-
```
|
|
560
|
-
|
|
561
|
-
Returns `{ element, isVisible }` — bind `element` to your target, `isVisible` becomes `true` once, then observer unobserves.
|
|
562
|
-
|
|
563
|
-
### useIntersection
|
|
564
|
-
|
|
565
|
-
For repeating animations:
|
|
566
|
-
|
|
567
|
-
```typescript
|
|
568
|
-
function useIntersection(
|
|
569
|
-
options?: {
|
|
570
|
-
threshold?: number;
|
|
571
|
-
rootMargin?: string;
|
|
572
|
-
root?: Element | null;
|
|
573
|
-
},
|
|
574
|
-
onVisible?: (isVisible: boolean) => void
|
|
575
|
-
): { element: HTMLElement | null; isVisible: boolean }
|
|
576
|
-
```
|
|
577
|
-
|
|
578
|
-
Returns `{ element, isVisible }` — `isVisible` toggles `true`/`false` on each scroll pass.
|
|
579
|
-
|
|
580
|
-
### animate Action
|
|
581
|
-
|
|
582
|
-
For direct DOM animation control without component wrapper:
|
|
583
|
-
|
|
584
|
-
```typescript
|
|
585
|
-
function animate(
|
|
586
|
-
node: HTMLElement,
|
|
587
|
-
options?: {
|
|
588
|
-
animation?: AnimationType; // Default: 'fade-in'
|
|
589
|
-
duration?: number; // Default: 800
|
|
590
|
-
delay?: number; // Default: 0
|
|
591
|
-
offset?: number; // Optional trigger offset
|
|
592
|
-
threshold?: number; // Default: 0
|
|
593
|
-
rootMargin?: string; // Optional custom margin
|
|
594
|
-
}
|
|
595
|
-
): { update?: (newOptions) => void; destroy?: () => void }
|
|
596
|
-
```
|
|
597
|
-
|
|
598
|
-
**Example:**
|
|
314
|
+
### Hero Section
|
|
599
315
|
|
|
600
316
|
```svelte
|
|
601
|
-
<
|
|
602
|
-
|
|
603
|
-
</
|
|
317
|
+
<div use:runeScroller={{ animation: 'fade-in-down', duration: 1000 }}>
|
|
318
|
+
<h1>Welcome</h1>
|
|
319
|
+
</div>
|
|
320
|
+
|
|
321
|
+
<div use:runeScroller={{ animation: 'fade-in-up', duration: 1200 }}>
|
|
322
|
+
<p>Engaging content</p>
|
|
323
|
+
</div>
|
|
604
324
|
|
|
605
|
-
<div use:
|
|
606
|
-
|
|
325
|
+
<div use:runeScroller={{ animation: 'zoom-in', duration: 1000 }}>
|
|
326
|
+
<button>Get Started</button>
|
|
607
327
|
</div>
|
|
608
328
|
```
|
|
609
329
|
|
|
610
330
|
---
|
|
611
331
|
|
|
612
|
-
##
|
|
613
|
-
|
|
614
|
-
### Core Layer Architecture
|
|
615
|
-
|
|
616
|
-
**Bottom Layer - Browser APIs & Utilities:**
|
|
617
|
-
1. **animations.ts** - Animation type definitions, validation, and utilities
|
|
618
|
-
2. **dom-utils.svelte.ts** - Reusable DOM manipulation utilities (CSS variables, animation setup, sentinel creation)
|
|
619
|
-
3. **useIntersection.svelte.ts** - IntersectionObserver composables for element visibility detection
|
|
620
|
-
|
|
621
|
-
**Top Layer - Consumer API:**
|
|
622
|
-
4. **runeScroller.svelte.ts** - **Recommended** - Sentinel-based action for scroll animation triggering
|
|
623
|
-
5. **animate.svelte.ts** - Alternative action for direct DOM node animation control
|
|
624
|
-
|
|
625
|
-
**Styles:**
|
|
626
|
-
- **animations.css** - All animation keyframes & styles (14 animations, GPU-accelerated)
|
|
627
|
-
|
|
628
|
-
### Key Principles
|
|
332
|
+
## 🔗 Links
|
|
629
333
|
|
|
630
|
-
- **
|
|
631
|
-
- **
|
|
632
|
-
- **
|
|
633
|
-
- **
|
|
634
|
-
- **DRY (Don't Repeat Yourself)** : Utility functions eliminate code duplication
|
|
635
|
-
- **Optimal DOM Manipulation** : Uses `cssText` for efficient single-statement styling
|
|
334
|
+
- **npm Package**: [rune-scroller](https://www.npmjs.com/package/rune-scroller)
|
|
335
|
+
- **GitHub**: [lelabdev/rune-scroller](https://github.com/lelabdev/rune-scroller)
|
|
336
|
+
- **Documentation**: [CLAUDE.md](./CLAUDE.md)
|
|
337
|
+
- **Changelog**: [CHANGELOG.md](./CHANGELOG.md)
|
|
636
338
|
|
|
637
339
|
---
|
|
638
340
|
|
|
639
|
-
##
|
|
640
|
-
|
|
641
|
-
### Recent Improvements (v1.1.0)
|
|
642
|
-
|
|
643
|
-
**DOM Utility Extraction**
|
|
644
|
-
- Extracted repeated DOM manipulation patterns into reusable utilities (`dom-utils.svelte.ts`)
|
|
645
|
-
- `setCSSVariables()` - Centralizes CSS custom property management
|
|
646
|
-
- `setupAnimationElement()` - Consistent animation class/attribute setup
|
|
647
|
-
- `createSentinel()` - Optimized sentinel creation using single `cssText` statement
|
|
648
|
-
- **Result**: Reduced code duplication, improved maintainability, cleaner codebase
|
|
649
|
-
|
|
650
|
-
**Memory Leak Fixes**
|
|
651
|
-
- Fixed potential memory leaks in repeat mode by tracking observer connection state
|
|
652
|
-
- Observer now properly disconnects in destroy lifecycle
|
|
653
|
-
- Prevents accumulation of observers on long-scroll pages
|
|
654
|
-
- **Result**: Better performance on content-heavy sites with many animations
|
|
341
|
+
## 📄 License
|
|
655
342
|
|
|
656
|
-
|
|
657
|
-
- Fixed `animate.svelte.ts` to properly handle dynamic threshold/rootMargin changes
|
|
658
|
-
- Observer now recreates when trigger options change at runtime
|
|
659
|
-
- Maintains correct state throughout component lifecycle
|
|
660
|
-
- **Result**: More reliable dynamic animation updates
|
|
661
|
-
|
|
662
|
-
**Bundle Size Optimization**
|
|
663
|
-
- Updated `.npmignore` to exclude test files from npm distribution
|
|
664
|
-
- Removes `*.test.ts`, `*.test.js` and built test files
|
|
665
|
-
- **Result**: ~3.6 KB reduction in package size
|
|
666
|
-
|
|
667
|
-
### Performance Impact
|
|
668
|
-
|
|
669
|
-
- **Code Size**: Reduced duplication without sacrificing readability
|
|
670
|
-
- **Runtime Performance**: Fewer DOM operations via optimized `cssText` usage
|
|
671
|
-
- **Memory Efficiency**: Proper observer cleanup prevents memory leaks
|
|
672
|
-
- **Bundle Size**: Test files excluded from distribution
|
|
343
|
+
MIT © [ludoloops](https://github.com/ludoloops)
|
|
673
344
|
|
|
674
345
|
---
|
|
675
346
|
|
|
676
|
-
##
|
|
677
|
-
|
|
678
|
-
- **IntersectionObserver** : Native browser API, no scroll listeners
|
|
679
|
-
- **CSS Transforms** : Hardware-accelerated (GPU)
|
|
680
|
-
- **Lazy Loading** : Only animate visible elements
|
|
681
|
-
- **Memory Efficient** : Automatic cleanup on unmount
|
|
682
|
-
- **SSR Compatible** : No DOM access during hydration
|
|
347
|
+
## 🤝 Contributing
|
|
683
348
|
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
## 🎯 Development
|
|
349
|
+
Contributions welcome! Please open an issue or PR on GitHub.
|
|
687
350
|
|
|
688
351
|
```bash
|
|
689
|
-
#
|
|
352
|
+
# Development
|
|
690
353
|
pnpm install
|
|
691
|
-
|
|
692
|
-
# Dev server
|
|
693
354
|
pnpm dev
|
|
694
|
-
|
|
695
|
-
# Type checking
|
|
696
|
-
pnpm check
|
|
697
|
-
|
|
698
|
-
# Type checking in watch mode
|
|
699
|
-
pnpm check:watch
|
|
700
|
-
|
|
701
|
-
# Format code
|
|
702
|
-
pnpm format
|
|
703
|
-
|
|
704
|
-
# Lint code
|
|
705
|
-
pnpm lint
|
|
706
|
-
|
|
707
|
-
# Build library for npm
|
|
708
|
-
pnpm build
|
|
709
|
-
|
|
710
|
-
# Run tests
|
|
711
355
|
pnpm test
|
|
712
|
-
|
|
713
|
-
# Preview built library
|
|
714
|
-
pnpm preview
|
|
356
|
+
pnpm build
|
|
715
357
|
```
|
|
716
358
|
|
|
717
359
|
---
|
|
718
360
|
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
- **Why "Rune"?** Svelte 5 uses **Runes** (`$state`, `$props()`) as core reactivity primitives
|
|
722
|
-
- **Zero Dependencies** : Pure Svelte 5 + Native Browser APIs (IntersectionObserver)
|
|
723
|
-
- **Extensible** : Add new animations by extending `animations.ts` and `animations.css`
|
|
724
|
-
- **Library Only** : This is the library repository. The demo website is in `rune-scroller-site`
|
|
725
|
-
- **Published as npm Package** : `rune-scroller` on npm registry
|
|
726
|
-
|
|
727
|
-
---
|
|
728
|
-
|
|
729
|
-
## 🔗 Links
|
|
730
|
-
|
|
731
|
-
- [Live Demo](https://runescroller.lelab.dev/) - Interactive showcase of all animations
|
|
732
|
-
- [npm Package](https://www.npmjs.com/package/rune-scroller) - Install from npm
|
|
733
|
-
- [Svelte 5 Documentation](https://svelte.dev)
|
|
734
|
-
- [IntersectionObserver MDN](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API)
|
|
735
|
-
- [LeLab.dev](https://lelab.dev)
|
|
736
|
-
- [GitHub Repository](https://github.com/ludoloops/rune-scroller)
|
|
737
|
-
|
|
738
|
-
---
|
|
739
|
-
|
|
740
|
-
## 📄 License & Credits
|
|
741
|
-
|
|
742
|
-
**MIT License** — Free for personal and commercial use.
|
|
743
|
-
|
|
744
|
-
Made with ⚡ by **[ludoloops](https://github.com/ludoloops)** at **[LeLab.dev](https://lelab.dev)**
|
|
745
|
-
|
|
746
|
-
**Open Source Project** — Contributions, issues, and feature requests are welcome!
|
|
361
|
+
Made with ❤️ by [LeLab.dev](https://lelab.dev)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rune-scroller",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
4
4
|
"description": "Lightweight, high-performance scroll animations for Svelte 5. ~2KB bundle, zero dependencies.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -40,19 +40,6 @@
|
|
|
40
40
|
"peerDependencies": {
|
|
41
41
|
"svelte": "^5.0.0"
|
|
42
42
|
},
|
|
43
|
-
"scripts": {
|
|
44
|
-
"dev": "vite dev",
|
|
45
|
-
"build": "svelte-package && node scripts/fix-dist.js",
|
|
46
|
-
"preview": "vite preview",
|
|
47
|
-
"prepare": "svelte-kit sync || echo ''",
|
|
48
|
-
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
|
49
|
-
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
|
50
|
-
"format": "prettier --write .",
|
|
51
|
-
"lint": "prettier --check . && eslint .",
|
|
52
|
-
"prepublishonly": "pnpm run check && pnpm run build",
|
|
53
|
-
"test:unit": "vitest",
|
|
54
|
-
"test": "npm run test:unit -- --run"
|
|
55
|
-
},
|
|
56
43
|
"devDependencies": {
|
|
57
44
|
"@eslint/compat": "^1.4.0",
|
|
58
45
|
"@eslint/js": "^9.36.0",
|
|
@@ -72,5 +59,17 @@
|
|
|
72
59
|
"typescript-eslint": "^8.44.1",
|
|
73
60
|
"vite": "^7.1.7",
|
|
74
61
|
"vitest": "^3.2.4"
|
|
62
|
+
},
|
|
63
|
+
"scripts": {
|
|
64
|
+
"dev": "vite dev",
|
|
65
|
+
"build": "svelte-package && node scripts/fix-dist.js",
|
|
66
|
+
"preview": "vite preview",
|
|
67
|
+
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
|
68
|
+
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
|
69
|
+
"format": "prettier --write .",
|
|
70
|
+
"lint": "prettier --check . && eslint .",
|
|
71
|
+
"prepublishonly": "pnpm run check && pnpm run build",
|
|
72
|
+
"test:unit": "vitest",
|
|
73
|
+
"test": "npm run test:unit -- --run"
|
|
75
74
|
}
|
|
76
|
-
}
|
|
75
|
+
}
|