# Digit Flip & Roll Transitions Two ways to animate a digit changing. Both are driven by the frame (deterministic) so they render identically every time — no CSS transition clock that the renderer can disagree with. Pick by tone: | Effect | Feel | Best for | |---|---|---| | Odometer reel | smooth, continuous glide | launch countdowns, premium/minimal timers | | Flip card | mechanical "snap", split-flap board | sale urgency, retro/airport-board style | ## Shared idea A single displayed digit is a stack of glyphs `0..9` laid out vertically. Showing digit `d` means translating the stack so glyph `d` sits in the window: `translateY(-d × digitHeight)`. Animate the *offset*, not the text, and you get a roll. The trick for a countdown is that the offset is continuous: between whole seconds the digit eases from its old value toward its new one. ## Odometer reel (Remotion, frame-driven) ```tsx import React from "react"; import { useCurrentFrame, useVideoConfig, interpolate, Easing } from "remotion"; const DIGIT_H = 160; // px per glyph const ROLL_FRAMES = 12; // how long a single digit roll takes // Animated single digit: rolls from `prev` to `value` over the last ROLL_FRAMES // of the current second. const Reel: React.FC<{ value: number; prev: number; secondProgress: number }> = ({ value, prev, secondProgress, }) => { // secondProgress: 0→1 across the current whole second. // Start the roll near the end of the second so the number "settles" on change. const t = interpolate(secondProgress, [0.7, 1], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp", easing: Easing.bezier(0.22, 1, 0.36, 1), }); // continuous position between prev and value const shown = prev + (value - prev) * t; const offset = -shown * DIGIT_H; return (
{[0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map((n) => (
{n}
))}
); }; // Renders an mm:ss reel where each digit rolls. export const ReelClock: React.FC<{ durationInSeconds: number }> = ({ durationInSeconds }) => { const frame = useCurrentFrame(); const { fps } = useVideoConfig(); const elapsed = frame / fps; const remaining = Math.max(0, Math.ceil(durationInSeconds - elapsed)); const prevRemaining = Math.max(0, remaining + 1); // value one second ago const secondProgress = elapsed % 1; // 0→1 within the current second const digits = (s: number) => [Math.floor(s / 600) % 10, Math.floor(s / 60) % 10, Math.floor(s / 10) % 10, s % 10]; const cur = digits(remaining); const prev = digits(prevRemaining); return (
{cur.map((d, i) => ( {i === 2 &&
:
}
))}
); }; ``` Notes: - Each digit only rolls when it actually changes; a digit whose `prev === value` sits still because `shown` stays constant. - A `9 → 0` rollover rolls *backward* through the strip (the cheap-looking part). To make tens/units roll the natural way on a carry, render an extra `0` glyph below the `9` and clamp — covered as an exercise; for most timers the backward roll at the tens boundary is unnoticeable at speed. - `ROLL_FRAMES` is unused above because the roll is tied to `secondProgress`; expose it instead if a fixed-length roll is preferred. ## Flip card (split-flap) The top half of the old digit folds down over a static bottom half, revealing the new digit. Pure CSS keyframe version — fine for a live web page; for Remotion, drive the `rotateX` off `secondProgress` the same way as the reel so it stays deterministic. ```css .flip { position: relative; width: 110px; height: 160px; perspective: 600px; } .flip .card { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; font-size: 140px; font-weight: 800; font-variant-numeric: tabular-nums; backface-visibility: hidden; } .flip .top { clip-path: inset(0 0 50% 0); transform-origin: bottom; } .flip .bottom { clip-path: inset(50% 0 0 0); } /* on change, animate the leaf folding down */ .flip .leaf { position: absolute; inset: 0; transform-origin: bottom; animation: fold .45s cubic-bezier(.36,0,.66,-0.2) forwards; } @keyframes fold { 0% { transform: rotateX(0deg); } 100% { transform: rotateX(-90deg); } } ``` ```js // swap the digit and replay the fold whenever the value changes function setFlip(el, newDigit) { if (el.dataset.v === String(newDigit)) return; // no change → no flip el.dataset.v = String(newDigit); el.querySelector(".top").textContent = newDigit; el.querySelector(".bottom").textContent = newDigit; const leaf = el.querySelector(".leaf"); leaf.textContent = newDigit; leaf.style.animation = "none"; void leaf.offsetWidth; // reflow → restart the animation leaf.style.animation = ""; } ``` Drive `setFlip` from the same `requestAnimationFrame` + `performance.now()` loop in the cookbook (only call it when the displayed second changes), so the flips fire on real time and never drift. ## Choosing - **Launch / premium:** odometer reel, slow ease, large tabular digits, muted palette. - **Flash sale:** flip card for the mechanical urgency, or reel with a faster roll and a hot accent color on `mm:ss`. - **Stream "starting soon":** either works, but keep it calm — a single reel on `mm:ss` reads better than a busy split-flap for a 5–10 minute hold.