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.

Loading indicatorCSS @keyframes, looping forever. Happo finds it by itself and captures one loop, so the snapshot repeats without a stutter.
Hippo loaderA canvas drawn by a web worker. A small driver hands Happo the grid’s own 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 trigger adds a class, clicks, focuses or hovers an element right before the capture.
Discovery and stages
discovery is for animations that start late, like a staggered list, and stages for sequences where each step starts the next.
Virtual clock
clock: 'virtual' replaces the page's clock so requestAnimationFrame loops can be stepped one frame at a time.
Expectations
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.
Sampling
Frames are spread evenly at 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.

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.

This chart is scroll-driven: its line draws and its callout appears as you scroll it into view. A story like this one captures it.
Chart.stories.ts
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-range plays 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.

KindCaptured
CSS animations (@keyframes)Yes
CSS transitionsYesWith a trigger to start them
element.animate() (Web Animations API)Yes
SVG SMIL animationsYes
Scroll-driven animations (scroll(), view())YesWith scrollTimelines: 'seek'
requestAnimationFrame loopsYesWith the virtual clock
Lottie, canvas engines, anything with its own frame loopYesWith a driver
<video>YesWith the built-in video driver
View transitionsNot yet
Animated GIF, WebP, and APNG imagesNo

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:

PixelGridLoader.stories.tsx
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.

Spinner.stories.ts
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.

Need help?