← All notesNote 02 / 05

We didn’t keyframe a single animation on our new site

Category
Ideas
Topic
Motion
Published
Oct 13, 2026
Reading time
6 min

Our shop menu is 33,000 lines of animation. The script that writes it is 859.

Motion · 6 min read

The © at the bottom of our new website isn’t a character. It’s a ring with a small C drawn inside it, because the font subset we ship has no copyright sign. We found out when one of our scripts refused to build. Another stopped on the euro sign. Without those checks, both would have shipped as empty boxes.

Scripts like that drew every animation on this site. Twenty Rive files run here: a rotary menu, eight capability cards, a footer bento, a shop menu that opens straight into checkout, four product videos, a studio strip, three blog covers and the subscribe card at the end of this post. We didn’t keyframe any of them.

We didn’t write most of the Python either. An AI coding agent did, while we directed with references, timing notes, “slower” and “no”. Our clients already know we build with AI. What they hire us for is the direction and the setup, not the typing. This is what that looks like on a real project.

Animation as text

Rive now has a command-line tool. It compiles RML, a text format that describes a Rive file in XML, into the .riv files its runtimes play. You can build, verify, inspect and screenshot a scene without opening the editor. And Rive knows who the user is: rive create drops an AGENTS.md and a CLAUDE.md into every new project, so a coding agent can look things up and check its own work.

Once an animation is text, a script can write it. Here is one, playing live:

The “Be found” video from our shop. One Python file of about 200 lines wrote it.
bb, bt = typed("black", 112, c(INK), "found", 80, per=4, x=CX, y=CY - 18)
handles, sel = selection(bt, 106, 124)
t_google = sticker("on Google", LIME_, CX - 84, CY + 96, -0.06)
t_ai = sticker("in ChatGPT & Claude", MAG_, CX + 36, CY + 156, 0.05)
pop(t_google, 112, dur=12)
pop(t_ai, 120, dur=12)
scenes.append(show(node("B found", [t_ai, t_google] + handles + [bb, sel]), 78, 178))
The scene you just watched: type the word, select it, pop two stickers. The numbers are frames.

The 859-line script

RML is an honest picture of a Rive file, which means it’s long. Our shop menu is about 33,000 lines of it. The script that writes it is 859.

The generators share a small kit with a short vocabulary. typed() writes a word letter by letter with a caret. selection() draws a highlight with handles. ring() wraps a phrase around a circle. sticker() and the ball mascot came later. A card reads like a storyboard: a list of scenes on a timeline. Each finished card weighs about 40 to 175 KB, fonts included.

What code buys

The shop’s offers live in one JSON file. The Rive shop menu and the footer tiles both read it. When an offer changes, we edit that file, rerun the generators and redeploy. Nobody opens an animation editor to retype a line. The four product videos still carry their text in their own scripts, so moving them onto the same file is next.

For a client, that’s the point: animated doesn’t have to mean frozen.

What code can’t see

The agent can put a tap at frame 148 of a chat animation. It can’t tell you the tap is early.

Every card went through rounds of frame sheets and screenshots before it felt right, and the screen kept teaching us rules the code couldn’t. A keyframe that lands has to hold, or Rive interpolates it toward the loop’s reset key and the blocks quietly drift. Our preview tool emulated a pixel ratio of 2 on a 1.6 display, and Rive drew everything about 1.25 times too big. At native size the same file was pixel-perfect.

Performance taught the same lesson. When the capability section scrolled into view, all eight cards started at once, and the laptop showed 45, 30 and 21 ms frames right after they mounted. Shorter loops wouldn’t have helped, because loop length costs nothing per frame. Timing did. Today a card only starts when the fan has turned close to it, from a file that was downloaded and parsed in an idle moment ahead of its turn. In a phone-sized window on the same laptop, the worst frame through that section is now 9 ms. Neither number comes from a real phone.

Then we scrolled the site on a real phone. The footer canvas is tall, and it trapped the thumb.

No generator would have flagged any of that. Finding it is judgment, and judgment is the expensive part now.

Where we wouldn’t use it

For a single organic character animation, a person in the Rive editor is still faster. Generators pay off when animations share a visual language or the copy keeps changing. Our site has both. So does most SaaS marketing: feature cards, product tours and launch pages that change with every release.

Field notes

  • Browsers cap how many WebGL contexts a page can hold. Chrome’s limit is 16 per render process, and past it the oldest context is dropped. @rive-app/react-webgl2 shares one offscreen context across every canvas by default; we measured 12 canvases on one context. In the plain web runtime that option is off by default, so turn it on.
  • When many canvases play the same file, parse it once and share it. Our blog list showed each cover in up to 15 rows; playing only the rows near the centre, from one shared file, cut the slowest frame by more than half.
  • A Rive canvas with listeners blocks touch scrolling unless you set isTouchScrollEnabled.
  • Start files when they come close to the viewport, not all at once.
  • Judge Rive at its native pixel ratio, ideally on a real phone.
  • If a Next.js dev server with a dozen Rive canvases hangs, check its dev log. Ours reached 1.4 GB, mostly console.assert calls that passed.
  • Plan for data binding. Rive now recommends it over state machine inputs and events. Ours still uses both, so that’s the next migration.

The agent wrote the Python. Our job was saying “no” until it was right.

You’re on the site already. Open the menu and spin the dial: a Python script drew it. Then open the shop. Its menu text and the footer tiles are generated from the JSON file above.

Book a 15-minute intro call

If you want motion like this on your site, we’ll tell you straight whether you need a generator or just a few good animations.

Next note ↗Wire, don’t replace