Animated fitness summary cards (Strava-style recaps)
Build a fitness recap video generator: distance, pace, and elevation tick up, a route map traces itself, and weekly streak dots fill in — exported as MP4.
A fitness recap video generator is a card with three big numbers, a traced route, and a row of streak dots — driven by user data and rendered to MP4 in one pass. This is the format Strava and Apple Fitness use because it survives on social: the card holds its meaning at thumbnail size, and the animation is what makes a screenshot want to be a video.
The polished version below animates distance, pace, and elevation count-ups, traces a SVG route path using stroke-dashoffset, and fills in seven streak dots one beat at a time. All three motions are driven by a single hf-seek listener.
What makes the format work
Three things, in order of importance:
- The numbers are the headline. Distance, pace, elevation — tabular numerals, 22px+, ticked up over two seconds. The card is a number announcement, not a chart.
- The route is texture. A single red SVG path on a near-black background. It does not need to be geographically accurate at recap size — it needs to feel like a route. Curves, not straight lines.
- The streak is the hook. Seven dots that fill in one-by-one give the viewer something to wait for. Without the streak, the card ends at 3 seconds. With it, the card has a story arc — numbers, then the why.
Tracing the route with stroke-dashoffset
This is the standard technique for "drawing" an SVG path. Set stroke-dasharray to the path length, set stroke-dashoffset to the same value (the line is invisible), then animate the offset down to zero.
const path = document.querySelector('.path');
const len = path.getTotalLength();
path.style.strokeDasharray = len;
path.style.strokeDashoffset = len;
addEventListener('hf-seek', (e) => {
const p = Math.max(0, Math.min(1, (e.detail.time - 0.4) / 2.2));
const ease = 1 - Math.pow(1 - p, 3);
path.style.strokeDashoffset = len * (1 - ease);
});getTotalLength() works on any SVG <path> and gives you the exact stroke length in user units. The cubic ease-out makes the draw fast at the start and settle at the end — same as drawing a line by hand. Linear interpolation feels mechanical here; the eye expects deceleration.
The count-up trio
All three stats animate from 0 to their final values on the same easing curve, with the same duration, in lock-step. Don't stagger them. Staggered stat reveals look like a slot machine; synchronized count-ups look like a unified result.
const f = +el.dataset.final; // 42.6
const d = +el.dataset.dec; // 1 decimal
el.textContent = (f * ease).toFixed(d) + ' km';Pace is the awkward one — it's 4'05"/km, not a single decimal number. Two ways to handle it: split into minutes and seconds and tick the seconds only, or skip the count-up for pace and just fade it in. The example above ticks the seconds digit; for a cleaner result, fade the whole pace block in at t=2.0s when the route is mostly drawn.
The streak dots
Seven dots, one per day. Filled means a workout happened. The animation fills them left-to-right with a 220ms gap between each — fast enough to feel like a sequence, slow enough that the eye registers each one.
dots.forEach((d, i) => {
const triggerTime = 3.2 + i * 0.22;
d.classList.toggle('on', t >= triggerTime);
});This is a step-function reveal, not a smooth interpolation. The dot is either filled or empty — there is no in-between state. That binary feel is what makes it read as "checked off a day" rather than "growing a chart."
Customize the card
Data shape for one-per-user renders
The template above takes seven fields and emits a 6-second MP4. Run it once per user, write the file to recaps/<userId>.mp4, ship it.
{
"userId": "u_8421",
"week": "May 13-19",
"distance_km": 42.6,
"pace": "4'52\"/km",
"elevation_m": 612,
"route_svg_path": "M20,110 C60,90 80,60 130,70 S200,40 240,50",
"streak_days": [true, true, false, true, true, true, true]
}This is exactly the programmatic video from data pattern. The route SVG path is precomputed server-side from GPS coordinates — a small simplification pass (Ramer-Douglas-Peucker, ~30 points) keeps the path file size sane and the trace animation readable.
Render to MP4
hyperframes render recap.html --out clip.mp4 --duration 6 --fps 30For Instagram Reels you want 9:16 — pass --width 1080 --height 1920 and adjust the card's max-width in CSS. The card already centers itself on the viewport, so the change is a single CSS variable. See the quickstart for the full flag reference, or open the playground to iterate without leaving your browser.
FAQ
How do I generate the route SVG from GPS coordinates?
Project lat/lng to x/y in a 320x140 viewBox (a simple linear scale based on the bounding box of the route), then run a simplification pass to drop redundant points. For a typical 10km run, ~30-50 points is enough — the human eye doesn't see the difference between a 50-point path and a 5000-point path at recap size.
Can I include the user's profile photo?
Yes, but it adds asset-loading complexity. For a batch pipeline, encode the photo as a base64 data URI in the template — the renderer doesn't need to wait on a network request, and the output stays deterministic. For one-off renders, a regular <img src> works.
What about cycling, swimming, or other sports?
Same template, different units and icon. The three-stat layout is sport-agnostic: distance, pace/speed, elevation/depth. For swimming, replace elevation with stroke count or SWOLF. The animation logic does not change.
How do I handle a missed day in the streak?
The example uses a boolean array. A missed day is just false — the dot stays in its empty state. If you want to emphasize the miss, give empty dots a slightly different border or a "rest" icon. Don't skip the dot entirely; the row should always show seven slots so the visual grammar is consistent across recaps.
What's the right duration for social?
Six seconds is the sweet spot: long enough to read the numbers, short enough that Reels/TikTok autoplay loops feel natural rather than draggy. For YouTube Shorts or a dedicated "year in review," push to 10-12s and add a second card with cumulative totals.
Related
- Animated number counter in HTML — the count-up technique in isolation
- Animated route map video — full-screen route reveals
- Batch personalized videos from CSV — the pipeline for one MP4 per user
Cite this postBibTeX · APA · Markdown
@misc{okafor2026animated,
author = {Marcus Okafor},
title = {Animated fitness summary cards (Strava-style recaps)},
year = {2026},
url = {https://hyperframes.video/blog/animated-fitness-summary-card},
note = {HyperFrames blog}
}Marcus Okafor. (2026, May 21). Animated fitness summary cards (Strava-style recaps). HyperFrames. https://hyperframes.video/blog/animated-fitness-summary-card
[Animated fitness summary cards (Strava-style recaps)](https://hyperframes.video/blog/animated-fitness-summary-card) — Marcus Okafor, 2026
Marcus leads design and motion at HyperFrames. Before that he shipped editorial motion for newsrooms and product launches. He thinks every easing curve has a personality.
Animated flight itinerary cards in HTML
Build an animated boarding pass video: a plane traces an arc from origin to destination, gate and time settle in, the pass folds out — rendered as MP4.
Animated poll results as MP4
Build a poll results animation that renders to deterministic MP4: four horizontal bars race to their final percentages, then a winner ribbon sweeps in.
Animated coupon and discount banners as MP4
A discount banner animation that counts up the percentage, wiggles its dashed edge, pulses urgency copy, and shimmer-strikes the original price. Built from HTML, rendered to MP4.
Building with HyperFrames? Come hang out.
We're on GitHub, in Discord, and the playground is one click away. Bring weird ideas — we collect them.