When frames are the only honest representation
Some animation is not describable as shapes with animated properties. Hand-drawn frame-by-frame work, a painted effect, a cel-animated character with squash that no rig would produce, a rendered 3D loop with real lighting. For these, the animation is the sequence of pictures, and the format that admits this is a sprite sheet: every frame laid out in a grid in one image, shown one at a time.
It sounds primitive. It is also the fastest possible playback, because there is no decode work per frame and no geometry to rebuild: the browser or GPU is showing a different window onto an image it already holds.
The CSS, which is shorter than people expect
One image, 24 frames in a row, each 200px wide:
.flame {
width: 200px; height: 200px;
background: url(/motion/flame.png) 0 0 / 4800px 200px;
animation: play 1s steps(24) infinite;
}
@keyframes play { to { background-position: -4800px 0; } }The whole technique is in steps(24). A normal timing function interpolates smoothly, which would slide the image continuously and show two half-frames at once. steps() holds each value and jumps, so the background position lands exactly on frame boundaries.
The end position is the full sheet width, not the width minus one frame. With steps(24), the animation is divided into 24 equal holds, and the last hold shows frame 24: the step after that would be the wrap. Getting this off by one is the classic bug: a flash of blank at the end of every loop means you subtracted a frame you should not have.
For a grid rather than a strip, animate background-position in two keyframe tracks, or use two nested animations: one stepping across columns fast, one stepping down rows slowly.
The number that matters is memory, not file size
A PNG sprite sheet might be 400KB on disk. That is not what it costs.
To display it, the browser decodes it to uncompressed pixels: width × height × 4 bytes. A 4800×200 strip is 3.8MB in memory. A 4096×4096 sheet (the size people reach for when they have a hundred frames) is 67MB, and that is per sheet, held for as long as the element exists.
This is the single most common way a sprite-sheet animation kills a phone. The symptoms are not a slow animation; they are the whole tab reloading, or other images being evicted and re-decoded, which looks like unrelated flickering elsewhere on the page.
Constraints that follow:
- Cap the total pixel area. Many mobile GPUs cap a single texture at 4096×4096; some older ones at 2048. Exceed it and the image may silently fail to render, or be downscaled.
- Keep the frame count honest. Twelve frames looping at 12fps reads as animation. Ninety-six frames at 60fps costs eight times the memory for a difference most viewers will not name.
- Use WebP for the sheet. Lossy WebP with alpha is typically a third of the PNG size on disk. The decoded memory is identical, so this only helps download, but download is still worth a third.
Choosing a frame rate deliberately
Frame-by-frame animation does not have to run at the display rate. Traditional animation has always worked “on twos” (a new drawing every second frame, so 12 drawings a second at 24fps) and it reads as smooth for most subjects while halving the work and the memory.
Fast action sometimes needs ones. Slow drifting motion can go on threes or fours. Choosing per shot rather than globally is how hand-drawn animation has always been budgeted, and it applies directly here: the frame count is your file size and your memory.
Where sprite sheets are still the right answer
- Short, looping, drawn effects: a flame, a sparkle, a loading character.
- Scroll-driven sequences where the user controls the frame. Showing frame 37 of 60 as a background-position is instant; seeking a video to an exact frame is not, because a video decoder must decode from the last keyframe forward.
- Anything inside a game engine, where texture atlases are the native idiom.
- Interfaces where the animation must not depend on a runtime library.
That second case is worth underlining. The product-page effect where an object rotates as you scroll is almost always a frame sequence, not a video, precisely because video seeking is unreliable and a sprite sheet’s random access is free.
Building one
# frames out of a source clip, 12 per second
ffmpeg -i source.mov -vf fps=12,scale=200:-1 frames/f_%03d.png
# assemble a single row
ffmpeg -i frames/f_%03d.png -filter_complex tile=24x1 sheet.png
# and a smaller version for download
cwebp -q 80 sheet.png -o sheet.webptile=6x4 gives a grid instead. ImageMagick’s montage does the same job with more control over padding, and padding matters: leave none, or GPU texture filtering will bleed the neighbouring frame’s edge pixels into yours.
The free path
Everything above is free. ffmpeg extracts and tiles. ImageMagick assembles and trims. Krita and OpenToonz are both open source and both do genuine frame-by-frame animation with onion skinning. OpenToonz is the tool Studio Ghibli’s in-house system became. Blender’s grease pencil does drawn animation inside a 3D scene and renders sequences directly. TexturePacker is the paid standard for atlases and free-tex-packer does the same job open source.
The limitation
Sprite sheets do not scale: in either sense. You cannot display one larger than it was drawn without softness, so a responsive layout means either shipping the largest size to everyone or shipping several sheets. And the cost grows linearly with every frame and every pixel, with no compression across frames at all, which is exactly the weakness that video codecs exist to fix.
The moment your sequence is longer than a couple of seconds, or larger than a few hundred pixels square, the arithmetic turns against you and a video becomes the correct format, with all the seeking limitations that brings back.
Every lesson here is open. Your progress is recorded once you pass the Video with AI exam.
Check your understanding
A 4096x4096 PNG sprite sheet compresses to 800KB on disk. A tester reports that on an older phone, opening the page causes unrelated images elsewhere to flicker and reload. What is the mechanism?
Pick the one you would defend. Your answer stays private.
Stuck on something?Ask Deepa, your AI tutor