Smooth, light-weight, performance-optimized image sequence scrolling, do that like Apple!
ApfelSequence allows you to create high-performance frame-by-frame scroll animations (like Apple & Sony) using HTML5 Canvas. It's framework-agnostic, responsive, and designed for speed.
- High Performance: Uses HTML5 Canvas for high fps rendering, avoiding heavy DOM reflows.
- Responsive Breakpoints: Load different image sequences for each breakpoint you provide to save bandwidth & fit better your UI.
- Smart Buffering: Choose between
eagerloading (all frames at once) orlazyloading (load on scroll position) to optimize initial load time and avoid bottlenecks. - Flexible Fit: Draw your frames as you want with
coverorcontainfit. - Scroll Trigger: Scroll trigger support for smooth scrolling experience, and reuse the GSAP scroll trigger configs.
- Reduced Motion: Respects user's
prefers-reduced-motionsettings automatically with a<img />simple tag as fallback. - Framework Agnostic: Core library written in pure TypeScript, with official wrappers for React, Vue, and JS.
| Package | Description | Size | Version |
|---|---|---|---|
@apfel-sequence/core |
The core engine | ||
@apfel-sequence/vanilla |
Wrapper for Vanilla JS | ||
@apfel-sequence/react |
Wrapper for React | ||
@apfel-sequence/vue |
Wrapper for Vue |
Using your preferred package manager:
# JS
npm install @apfel-sequence/vanilla
# React
npm install @apfel-sequence/react
# Vue
npm install @apfel-sequence/vue
HTML
<script src="https://unpkg.com/@apfel-sequence/vanilla@latest/dist/apfel-sequence.min.js"></script><script src="https://unpkg.com/@apfel-sequence/vanilla@latest/dist/apfel-sequence.min.js"></script>
<!-- ... -->
<!-- ...Setup your DOM -->
<div id="sequence-container" class="apfel-sequence container">
<canvas id="sequence-canvas" class="apfel-sequence canvas"></canvas>
</div>
<!-- ... -->
<!-- your JS -->
<script>
const sequence = new ApfelSequence({
container: document.querySelector('#sequence-container'),
canvas: document.querySelector('#sequence-canvas'),
assetsConfig: [
{
name: 'hero',
url: '/assets/images',
frameFirstId: 1,
frameLastId: 200,
frameDigits: 4,
frameSuffix: '.jpg'
}
],
scrollConfig: {
scrub: true,
start: 'top top'
}
});
// Clean up when dont need it anymore
// sequence.destroy();
</script>import { ApfelSequence } from '@apfel-sequence/react';
const assets = [
{
name: 'hero-sequence',
url: '/assets/sequence',
frameDigits: 4,
frameFirstId: 1,
frameLastId: 200,
frameSuffix: '.jpg'
}
];
export default function Hero() {
return (
<div style={{ height: '300vh' }}>
<ApfelSequence
assetsConfig={assets}
drawMode="cover"
scrollConfig={{
start: 'top top',
end: 'bottom bottom',
scrub: true
}}
/>
</div>
);
}<script setup>
import { ApfelSequence } from '@apfel-sequence/vue';
const assets = [{
name: 'hero-sequence',
url: '/assets/sequence',
frameDigits: 4,
frameLastId: 200
}];
</script>
<template>
<div style="height: 300vh">
<ApfelSequence
:assetsConfig="assets"
drawMode="cover"
/>
</div>
</template>Load smaller images for mobile devices to improve performance:
const assets = [
// Mobile: 0px - 768px
{
name: 'mobile-seq',
url: '/assets/mobile',
breakpointMin: 0,
breakpointMax: 768,
frameLastId: 100
},
// Desktop: 769px+
{
name: 'desktop-seq',
url: '/assets/desktop',
breakpointMin: 769,
frameLastId: 150
}
];| Option | Type | Default | Description |
|---|---|---|---|
assetsConfig |
Array |
[] |
Array of asset configurations |
drawMode |
String |
'cover' |
'cover' or 'contain' |
scrollConfig |
Object |
{} |
Scroll trigger configuration |
Todo:
- Implement networkPolicy, to be able to switch between adaptive and fallback-only policies automatically
Each object within the assetsConfig array defines a sequence of images and supports the following properties:
| Property | Type | Default | Description |
|---|---|---|---|
name |
string |
Required | Unique identifier for the asset sequence. |
url |
string |
Required | Base URL path where frames are located. |
frameFirstId |
number |
1 |
The index of the first frame. |
frameLastId |
number |
Required | The index of the final frame in the sequence. |
frameDigits |
number |
4 |
Number of digits for zero-padding (e.g., 4 → 0001.jpg). |
frameSuffix |
string |
Required | File extension (e.g., .jpg, .png, .webp). |
framePrefix |
string |
|
String prepended to the frame filename. |
breakpointMin |
number |
- | Minimum viewport width (px) to display this asset. |
breakpointMax |
number |
- | Maximum viewport width (px) to display this asset. |
frameFallback |
string | number |
- | Frame number or image URL to show before load. |
renderCanvas |
boolean |
true |
Whether to render the sequence on the canvas. |
You can control how frames are loaded (e.g., eager vs lazy, retry logic) using the loadingConfig prop.
| Option | Type | Default | Description |
|---|---|---|---|
loadingMode |
'eager' | 'lazy' |
'lazy' |
'eager' loads all immediately; 'lazy' waits for trigger. |
maxRetries |
number |
3 |
Attempts to retry failed frames. |
retryDelay |
number |
200 |
Delay (ms) between retries. |
preloadCount |
number |
(calculated) | Number of frames to force-load initially to be always ahead of the current scroll position of the users. |
- Sequential (lazy): Initial & progressive loading happens one-by-one to save bandwidth and to avoid network bottle-necks.
- Parallel (eager): Scrubbing/lazy-loading requests neighbors in parallel for speed, if your image sequences are too long or your images are too large, it may causes network bottle-necks.
- Resilience: Auto-retries failed frames (3 attempts by default).
This project is licensed under the MIT License.
by Mehmed Kurtic.