Spritesheet Sequencer
A WAAPI-powered spritesheet animator for squared grid-based spritesheets with stepped frame animation.
Click
Look
Mail
Pointer
Smudge
150ms
'use client'
import * as React from 'react'
import { Gauge, Pause, Play } from 'lucide-react'
import { SpritesheetSequencer } from '@/components/spritesheet-sequencer'
import { Button } from '@/components/ui/button'
import { Slider } from '@/components/ui/slider'
const SPRITESHEETS = [
{
name: 'Click',
src: 'https://qfxa88yauvyse9vr.public.blob.vercel-storage.com/spritesheets/click_2x2.png',
frameCount: 3,
},
{
name: 'Look',
src: 'https://qfxa88yauvyse9vr.public.blob.vercel-storage.com/spritesheets/look_2x2.png',
frameCount: 3,
},
{
name: 'Mail',
src: 'https://qfxa88yauvyse9vr.public.blob.vercel-storage.com/spritesheets/mail_3x3.png',
frameCount: 8,
},
{
name: 'Pointer',
src: 'https://qfxa88yauvyse9vr.public.blob.vercel-storage.com/spritesheets/pointer_2x2.png',
frameCount: 3,
},
{
name: 'Smudge',
src: 'https://qfxa88yauvyse9vr.public.blob.vercel-storage.com/spritesheets/smudge_2x2.png',
frameCount: 3,
},
]
const SPEED_MIN = 50
const SPEED_MAX = 500
export default function SpritesheetSequencerDemo() {
const [isPlaying, setIsPlaying] = React.useState(true)
const [speed, setSpeed] = React.useState(400)
// Invert: higher slider value = faster animation (lower frameDuration)
const frameDuration = SPEED_MIN + SPEED_MAX - speed
return (
<div className="flex min-h-[280px] flex-col items-center justify-center gap-6 px-6 py-12">
{/* Spritesheet grid */}
<div className="flex flex-wrap items-center justify-center gap-8">
{SPRITESHEETS.map((sprite) => (
<div key={sprite.name} className="flex flex-col items-center gap-2">
<div className="size-16">
<SpritesheetSequencer
src={sprite.src}
frameCount={sprite.frameCount}
frameDuration={frameDuration}
isPlaying={isPlaying}
/>
</div>
<span className="text-muted-foreground font-mono text-xs">
{sprite.name}
</span>
</div>
))}
</div>
{/* Controls */}
<div className="flex flex-wrap items-center justify-center gap-2">
<Button
variant="muted"
size="icon-sm"
onClick={() => setIsPlaying((p) => !p)}
>
{isPlaying ? <Pause /> : <Play />}
</Button>
<div className="bg-muted flex items-center gap-3 rounded-md px-3 h-8">
<Gauge className="text-muted-foreground size-4.5" />
<Slider
value={[speed]}
onValueChange={([value]) => setSpeed(value)}
min={SPEED_MIN}
max={SPEED_MAX}
step={25}
className="w-32 *:data-[slot='slider-track']:bg-muted-foreground/30"
/>
<span className="text-muted-foreground min-w-[5ch] font-mono text-xs">
{frameDuration}ms
</span>
</div>
</div>
</div>
)
}
Installation
Usage
import { SpritesheetSequencer } from '@/components/spritesheet-sequencer'
<div className="size-16">
<SpritesheetSequencer
src="/sprites/coin.png"
frameCount={4}
frameDuration={100}
className="[image-rendering:pixelated]"
/>
</div>Spritesheet Format
The component expects a squared spritesheet image with frames arranged in a grid:
frameCount = 4, gridSize = ceil(sqrt(4)) = 2
+-----+-----+
| 0 | 1 |
+-----+-----+
| 2 | 3 |
+-----+-----+Frames are read left-to-right, top-to-bottom. The grid size is automatically calculated as ceil(sqrt(frameCount)), so partial grids are supported (e.g., 14 frames in a 4×4 grid).
Props
| Prop | Type | Default | Description |
|---|---|---|---|
src | string | - | URL to the squared spritesheet image |
frameCount | number | - | Total number of frames in the spritesheet |
frameDuration | number | 100 | Duration per frame in milliseconds |
isPlaying | boolean | true | Whether the animation is playing |
loop | boolean | true | Whether to loop the animation |
direction | 'normal' | 'reverse' | 'alternate' | 'alternate-reverse' | 'normal' | Animation playback direction |
resetOnPlay | boolean | false | Reset to frame 0 when isPlaying changes to true |
onFrameChange | (frame: number) => void | - | Callback fired when the current frame changes |
onLoad | () => void | - | Callback fired when the spritesheet image loads |
onComplete | () => void | - | Callback fired when animation completes (non-loop) |
Examples
Basic coin animation
<SpritesheetSequencer
src="/sprites/coin.png"
frameCount={4}
frameDuration={100}
/>Controlled playback
const [isPlaying, setIsPlaying] = useState(true)
const [speed, setSpeed] = useState(100)
<SpritesheetSequencer
src="/sprites/explosion.png"
frameCount={16}
frameDuration={speed}
isPlaying={isPlaying}
loop={false}
resetOnPlay
onComplete={() => setIsPlaying(false)}
/>
<button onClick={() => setIsPlaying(p => !p)}>
{isPlaying ? 'Pause' : 'Play'}
</button>Pixel art (8-bit style)
For crisp pixel art, disable image smoothing:
<SpritesheetSequencer
src="/sprites/character.png"
frameCount={8}
frameDuration={80}
className="[image-rendering:pixelated]"
/>How It Works
The component uses the Web Animations API (WAAPI) with step-end easing to create discrete frame jumps.
The spritesheet is displayed via CSS background-image with calculated background-position keyframes for each frame in the grid.