New Animation testing
Your animations are UI too.
Spinners, toasts, skeletons, and transitions break like everything else, and most visual tests freeze them to a single frame. Happo steps each animation to exact moments and compares how it moves, frame by frame.
Start free with 5,000 snapshots a month. No credit card needed.
@keyframes, looping forever. Happo finds it by itself and captures one loop, so the snapshot repeats without a stutter.seek.In the review
A change to the motion is a diff.
This is the diff view from a Happo report: the hippo loader before and after a small change, where the dots now pulse green once per loop. The strip under it marks the frames that changed, darker the more they did. Pause it and step through them.
How it works
Seeking, not recording.
Happo pauses each animation and asks it to render at exact times, rather than filming it as it plays. The same animation gives the same frames on every run, so there’s no flaky video to squint at.
- Auto mode
mode: 'auto'captures an animated snapshot when the story has an animation Happo can drive, and a still image when it doesn't.- Triggers
- A CSS transition needs something to start it. A
triggeradds a class, clicks, focuses or hovers an element right before the capture. - Discovery and stages
discoveryis for animations that start late, like a staggered list, andstagesfor sequences where each step starts the next.- Virtual clock
clock: 'virtual'replaces the page's clock sorequestAnimationFrameloops can be stepped one frame at a time.- Expectations
expectsays what a capture has to find (animations, frames, stages), so a broken animation shows up in the report instead of becoming a still image.- Sampling
- Frames are spread evenly at
fpsby default. Withsamplingthey 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.
Scroll-driven animations
Even the ones that move as you scroll.
Reveals, progress bars, and charts that draw themselves as they come into view run on scroll position, not time. With scrollTimelines: 'seek', Happo plays them as if the page scrolled past at a steady speed.
export const ScrollingIn = {
parameters: {
happo: {
animate: {
mode: 'auto',
scrollTimelines: 'seek',
duration: 1500,
fps: 8,
},
},
},
};- Ranges keep their place. The scroll covers the stretch where something animates, and each
animation-rangeplays where it falls along it. A chart’s line draws before its callout appears, and a staggered row of reveals stays staggered. - Nothing actually scrolls. The snapshot frames the page as it is, and a story too short to scroll works too.
- Where browsers run them. Chrome, Edge, and Safari run scroll-driven animations. Firefox doesn’t yet, so a Firefox target shows your fallback styles.
Available since happo 6.23.0.
Coverage
What Happo can capture
Animated snapshots work in Chrome, Edge, Firefox, and Safari targets, with the Storybook, Playwright, Cypress, Vitest, pages, and custom integrations. iOS Safari targets don’t support them.
| Kind | Captured |
|---|---|
CSS animations (@keyframes) | Yes |
| CSS transitions | YesWith a trigger to start them |
element.animate() (Web Animations API) | Yes |
| SVG SMIL animations | Yes |
Scroll-driven animations (scroll(), view()) | YesWith scrollTimelines: 'seek' |
requestAnimationFrame loops | YesWith the virtual clock |
| Lottie, canvas engines, anything with its own frame loop | YesWith a driver |
<video> | YesWith the built-in video driver |
| View transitions | Not yet |
| Animated GIF, WebP, and APNG images | No |
Drivers
Bring your own frame loop
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: 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.
Get started
Start with one story.
Opt in one story at a time, with the animate parameter on a story that shows an animation. Every other story keeps its still screenshot.
export const Default = {
parameters: {
happo: {
animate: {
mode: 'auto',
prefersReducedMotion: false,
},
},
},
};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.
Test how it moves.
Turn on animated snapshots for one story this afternoon, and see your first frame-by-frame diff on the next pull request.
Start free with 5,000 snapshots a month. No credit card needed.