DEV Community

Shyan-Ming Perng
Shyan-Ming Perng

Posted on • Originally published at github.com

Choreo: an animation DSL whose programs verify their own rendered motion

Most animation tooling assumes a human at the wheel: a timeline, a scrubber, easing curves you nudge by eye. I wanted the opposite — a way to say what an animation should do and have something check that the pixels actually did it. That's Choreo.

The idea in one screen

You write a scene:

scene "signup-success" size 800x450 duration 2600ms background #f1f5f9

entities
  card  = rect 320x220 fill #ffffff radius 12
  ring  = circle 30 stroke #16a34a stroke-width 4
  tick  = path "M8 16 L14 22 L24 10" size 32x32 stroke #16a34a stroke-width 4
  label = text "Account created" size 18 weight 600 color #0f172a

layout
  center card
  inside ring card top 36
  center tick in ring
  below label ring gap 18

motion
  seq
    fadeIn card 500ms easeOut
    par
      pop ring 400ms
      drawOn tick 450ms easeOut
    slideIn label from bottom 10 400ms easeOut

checks
  during 0..500   : fadesIn(card)
  during 500..900 : grows(ring) and fadesIn(ring)
  at 1800         : below(label, ring)
  always          : inside(ring, card) and inside(label, card)
  at end          : settled(all)
Enter fullscreen mode Exit fullscreen mode

It compiles to a self-contained SVG + CSS animation. Then the checks block becomes a spec the verifier runs against the rendered result:

npx choreoc examples/signup-success.choreo -o out --verify
#  → out/signup-success.html      the animation
#  → out/signup-success.spec.mjs  the checks, as a verifier spec
#  → 19/19 checks passed
Enter fullscreen mode Exit fullscreen mode

Why not just screenshot-diff it?

Visual-regression tools (Chromatic, Percy, Applitools) compare frames. They're great at "this looks different from last time" and useless at "the toast slid in before it faded and had settled by 1.2s." Choreo's verifier samples the real trace — element positions, opacity, size over time, driven by Playwright — and asserts first-order predicates over it: movesLeft, fadesIn, inside(a, b), settled(all), with time windows (during, at, always, at end).

The verifier is a separate package and works on any web animation, not just Choreo output — CSS keyframes, GSAP, Motion, a Remotion export. If you have an animation and a claim about how it should move, you can check the claim:

export default {
  name: 'signup-flow',
  target: 'file:///…/signup-flow.html',
  duration: 2600,
  entities: { toast: '#toast', msg: '#msg' },
  checks: [
    { during: [0, 400], assert: ['movesLeft(toast)', 'fadesIn(toast)'] },
    { at: 1200,          assert: ['visible(toast)', 'inside(msg, toast)'] },
    { atEnd: true,       assert: ['fadedOut(toast)'] },
  ],
};
Enter fullscreen mode Exit fullscreen mode

Where this came from, and what it isn't

It began as an internal probe: can an LLM author animation reliably? On its own, not really — roughly half right. The research I leaned on (MoVer, SIGGRAPH 2025) shows that wrapping generation in a formal verify-and-correct loop closes most of that gap. Choreo is that shape made concrete: a grammar small enough for a model to hit from a spec, and a checker that catches it when it's wrong.

It is a research artifact, released as-is under MIT. No roadmap promise, no support SLA. There's a cross-framework benchmark in the repo if you want to poke at whether the DSL actually beats raw CSS (short version: the durable win is authoring cost, not correctness). Issues and PRs welcome; treat it as a foundation, not a maintained service.

Disclosure: this came out of JieGou, an ops-automation company I'm building; Choreo is an open side-output, not a product we sell.

Top comments (0)