Skip to content

Writing

2 min read

UI at 12fps

What if UI moved like Spider-Verse?

I posted a little video of this on X (opens in new tab) and it got a lot more attention than I expected, so here is the slightly longer version. These are real Base UI (opens in new tab) components animated at 12fps, the same way Spider-Verse animates its characters. Have a click around.

01Dialog5 in · 3 out

02Menu4 + 1 per item

03Popover4 in · 2 out

04Tooltip4 in · 2 out

05Switch & checkbox4 · 3

06Tabs4

Base UI does state and a11y.

CSS draws every pose.

No animation library.

07Accordion4

08Toast4 in · 2 out

What is “on twos”?

Film runs at 24fps. Animating on twos means each drawing is held for two of those frames, so the character only moves 12 times a second. Spider-Verse does this to the characters while the camera stays smooth, which is a big part of why it looks drawn rather than rendered.

UI usually does the opposite and interpolates every frame, keeping the motion as subtle as possible. I wanted to see what happens if you don’t.

Just drop the frames

The obvious first attempt is to take a normal, subtle animation and hold each frame. Both of these take the same amount of time, the one on the right just gets 2 drawings to do it in, and it ends up looking laggy rather than drawn.

Smoothevery frame

Laggy12fps

Try either side, both play

Push the poses

Animators get around this by exaggerating. Each drawing is a pose, squashed, overshooting, stretched and then settled. Same 12fps, but now every frame actually has something to say.

Laggy12fps

On twos12fps, big poses

Try either side, both play

Why not just go cartoon?

You could keep the big poses and interpolate between them. That’s the left side, it’s bouncy and fun but a bit floaty because the in-betweens smooth out all of the snap. Holding the poses on the right feels punchier, even with the same keyframes and the same timing.

Cartoonevery frame, big poses

On twos12fps, big poses

Try either side, both play

How it works

There is no animation library involved. Base UI handles the state and accessibility and exposes data attributes like data-open and data-closed, then CSS draws every pose.

Each keyframe stop is one drawing and steps(1, end) holds it until the next one, so the duration is just the number of drawings divided by the frame rate.

.dialog[data-open] {
  /* 5 drawings at 12fps */
  animation: pop-in calc(5s / 12) steps(1, end) both;
}
 
@keyframes pop-in {
  0% { transform: scale(0.45, 0.3) translateY(24px); }
  20% { transform: scale(1.14, 0.84); }
  40% { transform: scale(0.9, 1.12) translateY(-8px); }
  60% { transform: scale(1.04, 0.97); }
  80%, 100% { transform: none; }
}

Here are those 5 drawings, step through them one at a time. The faded copies are the earlier drawings, like onion skinning in an animation tool, so you can see how far the dialog jumps from one drawing to the next.

0mstransform: scale(0.45, 0.3) translateY(24px)

Transitions are a little trickier as steps() can’t overshoot, but linear() can, and if you repeat each stop you get a held frame. That’s what drives the tabs indicator and the accordion.

.tab-indicator {
  translate: var(--active-tab-left) 0;
  transition: translate calc(4s / 12)
    linear(0.3 0%, 0.3 25%, 1.2 25%, 1.2 50%, 0.94 50%, 0.94 75%, 1 75%, 1 100%);
}

What stays smooth

Anything you’re touching. If the slider thumb only moved 12 times a second it would just feel laggy, no matter how good the poses are. The thumb follows your pointer every frame and only the grab and the value bubble are drawn.

40

Spider-Verse keeps the camera smooth, UI should keep the input smooth.


Got any questions? Feel free to reach out on X - @danbillson (opens in new tab)