Motion for an app that has to wait.
Café Cal reads a photo of your plate and tells you what is on it. That takes a few seconds, and a calorie tracker is full of seconds like that. This is how I designed the motion that fills them, shipped it on two platforms from one token file, and how I work with Claude Opus 5.5 to do it. Every demo on this page is live code running the shipping values. Press things.
- My role Co-founder (one of three). Led motion design and the SwiftUI build.
- Team Three co-founders. [TODO: co-founders' roles]
- Duration March 2026 – present.
- Outcome About 35,000 downloads after we redesigned onboarding and the paywall, with marketing. The motion ships on iOS and Android from one token file.
- Feedback Feedback that changed the design: [TODO: one real example]
InteractivePress Replay to run the splash again
Almost no downloads at launch, then about 35,000.
Café Cal launched to almost no downloads and almost no revenue. So we redesigned onboarding and the paywall. With marketing on top, that took it to about 35,000 downloads.
The rest of this page is the part I led: the motion, and the SwiftUI build it runs in.
Every wait in the app looked like something had broken.
An AI calorie tracker waits a lot. It waits on sign-in at launch, on the maths behind your plan, and on a model reading your plate. In our first builds each of those waits was a spinner or a hard cut. The worst was the camera. You pressed the shutter, the screen flashed white, and the app jumped to Home with your photo gone. The note we left ourselves in the code says it plainly: the swap "read as the app crashing rather than as a photo being taken."
None of this was a delight problem, and adding bounce would not have fixed it. People were asking three plain questions and the app answered none of them. Where did my photo go? Is it actually working? Did that count? The motion work was about answering those three questions, on both platforms, without saying anything untrue.
One job for each question, and a curve to match.
The token file opens with its own grammar: physics-based springs for interactive, easeOut for data fill. Everything below follows from that line and three jobs.
.smooth(0.5)Continuity. Anything you create travels to where it lives. It never teleports. Where precision matters, the curve does not overshoot.
linear · 5.0s → 1.2sHonest waiting. Show the real stages of the work. Show them in full once, then get out of the way.
spring(0.4, 0.85)Restraint. Celebrate only what the person chose to chase. Stay quiet about everything else. Frequent actions get the least motion.
The photo you take is the photo that lands on the card.
The fix was to stop cutting and fly the photo. The shutter freezes the frame for 50 ms so the moment registers as capture, not failure. The photo then lifts onto an overlay above the tab bar, because SwiftUI's matched geometry cannot cross a tab switch. It flies into the thumbnail on the new processing card while a black backdrop hides the swap and dissolves. The landing is a light tap on the wrist.
The flight uses .smooth and not a spring, and the token comment says why: an overshoot would leave the flying photo a few points off. Bounce is expressive. Here it would just be wrong.
InteractivePress Replay, or slow it to ¼× with the speed control
Show the work once. The second time, just give the answer.
Buell and Norton's labor illusion research is the reason the plan screen does not appear instantly. When a service shows the work it is doing, people prefer it even over an instant result. In one of their studies, 62% of people chose a 30-second wait with visible progress, against 42% when the wait was blind.1 The same paper sets the limit I design to: effort cannot rescue a bad result.
So the first plan takes five seconds and names four real stages as it goes. Regenerating it takes 1.2. You have already watched the work once, and making you watch it again would be theatre. The meal scan follows the same rule, with a café voice. Frothing up the macros is a joke, but the skeleton it sits on is held for at least 900 ms, so a fast answer never flickers.
InteractiveTap Build the plan to watch the wait
Where your finger leads, the motion follows and adds nothing.
The ruler picker sets weight, height, goal and pace, so it is the most-touched control in onboarding. It gets no easing flourish. It follows the finger one to one and snaps to each step. Every value you cross is a medium haptic detent, so you can feel 0.5 kg without looking. The number rolls only the digits that changed. "Use recommended" glides there in a quarter-second and clicks through every value on the way, so the jump still feels like it was made with a hand.
InteractiveDrag the ruler, or tap Use recommended
We shipped confetti for a biscuit, then took it back.
Hitting a goal earns the biggest moment in the app. A disc springs in, a check draws itself, and 90 pieces of confetti fall in that goal's own colour. Our first version used the same trigger for every nutrient, including the ones with a cap, so going over your sugar limit set off a celebration. On August 10 we replaced it with a warning. On September 21 we softened the warning too, because the rejection buzz was, in the commit's words, "literally a rejection buzz for eating a biscuit."
On September 24 we limited each goal to one celebration per day. Anything that happens often has to become quiet, or it stops meaning anything.
InteractiveLog a protein shake or a biscuit
I write the intent in words. The model writes the curve.
Motion is hard to hand off because it lives between a design file and code. My approach is to put the intent into the token file as a sentence. The splash accent's comment reads "an overshoot would read playful, which is the wrong register for a high-contrast serif wordmark." Opus 5.5 can act on a sentence like that. It proposes the curve and writes the SwiftUI. It ports the result to Compose with the conversion written at the top of the Android token file, and records every place the platforms have to differ.
"Like a garnish set on a plate." What it should feel like and what it must not do.
Picks spring or tween, writes the values and the reasoning into the token.
At full speed and at a quarter. With Reduce Motion on. With a thumb, not a cursor.
Compose from the same numbers. The web version on this page. What drifted.
The spring lab below shows the whole idea in one place: one token and three platforms. Choose a token from the app and you get the SwiftUI, the Compose conversion, and a CSS linear() curve sampled from the same physics. Every demo on this page was rebuilt from those numbers.
InteractiveDrag the sliders or pick a token, then press Play
The part I trust it with most is the audit. For this page, parallel Opus agents read both codebases end to end and gave me back every animation, haptic and Reduce Motion check, with line numbers. They also found what I had missed.
Motion is a sentence the app says. It should be true.
Write the reason next to the number. A spring value without a sentence gets "improved" by the next person, or the next model. A spring value with a sentence gets ported correctly to a platform I did not write it for.
The model is fast. My thumb is the judge. Opus can write, port and audit a spring in seconds. It cannot tell me that a flying photo feels a few points off. I still decide that on a phone, at a quarter speed, with the sound off.
References
- Buell, R. W. and Norton, M. I. (2011). The Labor Illusion: How Operational Transparency Increases Perceived Value. Management Science 57(9).
- Apple, WWDC23. Animate with springs. Also the Human Interface Guidelines on motion.
- Emil Kowalski, You don't need animations. The frequency rule behind section 07.
- Anthropic, Claude Opus 5.5.