Happo normally freezes animations and takes a single screenshot. With animated snapshots, it steps an animation through a series of times, takes a screenshot at each one, and stores the frames as one animated PNG. A change to the motion shows up as a diff.
We capture Happo's own animations this way.
A CSS @keyframes animation that loops forever. Happo finds it by itself and captures one iteration, so the snapshot loops without a stutter.
Drawn on a canvas by a web worker, in its own frame loop. Happo can't see into that, so a small driver hands it the grid's own seek. It holds still if you prefer reduced motion.
This is the diff view from a Happo report, showing the hippo loader before and after a small change: the dots that make up the hippo now pulse green once per loop. Some frames are identical, and the strip under the diff marks the ones that changed, darker the more they did. Pause it and step through them.
mode: 'auto' captures an animated snapshot when the story has an animation Happo can drive, and a still image when it doesn't.trigger adds a class, clicks, focuses or hovers an element right before the capture.discovery is for animations that start late, like a staggered list, and stages for sequences where each step starts the next.clock: 'virtual' replaces the page's clock so requestAnimationFrame loops can be stepped one frame at a time.expect says what a capture has to find (animations, frames, stages), so a broken animation shows up in the report instead of becoming a still image.fps by default. With sampling they can be placed where the motion is, or at exact times.An animated snapshot counts as three snapshots, however many frames it holds. A story that comes out as a still image counts as one.
| Kind | Captured |
|---|---|
CSS animations (@keyframes) | Yes |
| CSS transitions | Yes, with a trigger to start them |
element.animate() (Web Animations API) | Yes |
| SVG SMIL animations | Yes |
requestAnimationFrame loops | Yes, with the virtual clock |
| Lottie, canvas engines, anything with its own frame loop | Yes, with a driver |
<video> | Yes, with the built-in video driver |
| Animated GIF, WebP and APNG images | No |
| Scroll-driven animations | No |
| View transitions | Not yet |
Animated snapshots work in Chrome, Edge, Firefox and Safari targets, and with the Storybook, custom, pages, Cypress and Playwright integrations. iOS Safari targets don't support them.
An animation that runs its own frame loop, like Lottie or a canvas engine, never goes through the browser's animation APIs, so Happo can neither find nor seek it. A driver teaches it how: it has a name and a discover function that returns a handle for each animation, with its length and a seek that renders it at a given time.
This is the driver for the hippo loader above:
import { registerAnimationDriver } from 'happo/storybook/register';
import { pixelGridsIn } from './pixelGrid';
registerAnimationDriver({
name: 'pixel-grid',
discover: (root) =>
pixelGridsIn(root).map((grid) => ({
target: grid,
element: grid.canvas,
durationMs: grid.loopMs,
repeats: true,
seek: grid.seek,
pause: grid.pause,
release: grid.release,
})),
});Happo seeks a driver's handles on the same timeline as everything else on the page. A video driver is built in.
Opt in one story at a time, with the animate parameter on a story that shows an animation:
export const Default = {
parameters: {
happo: {
animate: {
mode: 'auto',
prefersReducedMotion: false,
},
},
},
};Every other story keeps its still screenshot. Happo's targets prefer reduced motion by default, which is why prefersReducedMotion: false is there. The animated snapshots docs cover the rest: every option, the other integrations, hooks, drivers and troubleshooting.
Sign up to get started, or go to the docs if you already have an account.