← All work

Café Cal · Co-founder · Motion Design · SwiftUI + Compose · Claude Opus 5.5

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.

Role
Co-founder (one of three). Led motion design and the SwiftUI build
When
March 2026 – present. Capture flight and splash shipped August 10
System
One token file, ported value for value to Compose. About 140 haptic call sites
Tools
SwiftUI, Jetpack Compose, Claude Opus 5.5
Role and team
  • 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

Your nutrition companion

The splash, in three beats: reveal, hold while the app loads, exit.

RevealFeathered wipe, easeOut 0.7s, 0.18s line stagger
AccentDrops at 0.62s on .smooth(0.44). No bounce
HoldBreathes to 1.012 on easeInOut 2.6s while auth and billing load
ExitScale 1.06 and fade, easeInOut 0.45s
(01)   From launch to 35,000 downloads

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.

(02)   The problem

Every wait in the app looked like something had broken.

Problem statement

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.

(03)   Three jobs

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.

  1. .smooth(0.5)

    Continuity. Anything you create travels to where it lives. It never teleports. Where precision matters, the curve does not overshoot.

  2. linear · 5.0s → 1.2s

    Honest waiting. Show the real stages of the work. Show them in full once, then get out of the way.

  3. 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.

(04)   Continuity

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

Screen recording from the shipping iOS app, dark mode. The ¼× control slows it down.

Shutterspring(0.2, 0.6) to 0.85, medium impact
Freeze50 ms hold, chrome out in 0.05s
Flight.smooth(0.5), corner 0 → 18pt, backdrop easeOut 0.32s
Cardspring(0.42, 0.82), scale 0.96 from the top
Land540 ms, light impact, same-frame hand-off
WaitSweep 1.4s linear, prompt every 2.2s
(05)   Honest waiting

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

0%
Analyzing your profile…
Your plan is ready
Built from your answers. Every number cites its source.
1,850Calories
210gCarbs
120gProtein
60gFats
Cites 5 peer-reviewed sources

Tap Build the plan on the phone, or use the button, to run it again. Sample numbers.

Minimum5.0s first run, 1.2s after that
StagesCopy changes at 25, 50 and 75%, easeInOut 0.3s
RevealCross-fade 0.3s, then rings trace each card, easeOut 1.2s
Slow savePast the minimum + 0.3s, "Finalizing your plan…"
(06)   Touch

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

What's your goal weight?
kg
Drag, or use ← →
Ticks16pt apart. Major 36pt, medium 22, minor 11
DetentMedium impact on every step crossed
ReadoutnumericText, easeInOut 0.12s
RecommendGlide on easeOut 0.25s
(07)   Restraint

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

Today
Monday, Sept 28
Protein · 112 of 120 g
Sugar · 48 of 50 g cap
Quick log
i
You're past today's sugar cap52 of 50 g. Good to know, nothing to fix.
Protein goal hit
120 of 120 g today.

Tap a quick-log row on the phone, or use the buttons. Sample copy.

GoalDisc and glow on spring(0.4, 0.85), success haptic
CheckTraces at 0.27s, easeOut 0.38s
Confetti90 pieces, gravity 0.7t², spin ±4.7 rad/s
CapInfo toast, easeOut 0.4s, light tap only
(08)   Working with Opus 5.5

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.

01 · MeDescribe it physically

"Like a garnish set on a plate." What it should feel like and what it must not do.

02 · OpusPropose the curve

Picks spring or tween, writes the values and the reasoning into the token.

03 · MeJudge it on device

At full speed and at a quarter. With Reduce Motion on. With a thumb, not a cursor.

04 · OpusPort and audit

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

SwiftUI
Compose
CSS

stiffness = (2π / response)² · dampingRatio = dampingFraction

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.

Found
The onboarding slide, the Home stagger and the ruler ignore Reduce Motion on iOS.
Next
Fades in place of travel, matching the capture flight's reduced path.
Found
On Android the streak count-up jumps straight to its number, and seven motion tokens are never used.
Next
Wire the 0.7s count-up and delete the tokens nobody calls.
Found
The projection graph, the most persuasive screen in onboarding, does not animate at all.
Next
A single draw-on, after the numbers it is drawn from have settled.
(09)   What I'm keeping

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

  1. Buell, R. W. and Norton, M. I. (2011). The Labor Illusion: How Operational Transparency Increases Perceived Value. Management Science 57(9).
  2. Apple, WWDC23. Animate with springs. Also the Human Interface Guidelines on motion.
  3. Emil Kowalski, You don't need animations. The frequency rule behind section 07.
  4. Anthropic, Claude Opus 5.5.
Speed
Reduce motion