# Countdown Cookbook Complete, runnable recipes. Every value is derived from an authoritative clock (frame or timestamp) so nothing drifts. ## 1. Configurable Remotion countdown (mm:ss + dd:hh:mm:ss) A single composition that handles any duration and either format. Drop into `src/Countdown.tsx`. ```tsx import React from "react"; import { useCurrentFrame, useVideoConfig, AbsoluteFill } from "remotion"; const pad = (n: number) => String(n).padStart(2, "0"); const formatTime = (totalSeconds: number, mode: "mm:ss" | "dd:hh:mm:ss") => { const s = Math.max(0, totalSeconds); if (mode === "mm:ss") { return `${pad(Math.floor(s / 60))}:${pad(s % 60)}`; } const d = Math.floor(s / 86400); const h = Math.floor((s % 86400) / 3600); const m = Math.floor((s % 3600) / 60); const sec = s % 60; // drop leading day field once it is zero, for a cleaner read return d > 0 ? `${pad(d)}d ${pad(h)}:${pad(m)}:${pad(sec)}` : `${pad(h)}:${pad(m)}:${pad(sec)}`; }; export type CountdownProps = { durationInSeconds: number; format?: "mm:ss" | "dd:hh:mm:ss"; label?: string; }; export const Countdown: React.FC = ({ durationInSeconds, format = "mm:ss", label = "", }) => { const frame = useCurrentFrame(); const { fps } = useVideoConfig(); // ceil so the last whole second ("1") is shown for its full duration, // and the display reaches "0" exactly on the final frame. const remaining = Math.max(0, Math.ceil(durationInSeconds - frame / fps)); const ended = remaining === 0; return ( {label && (
{label.toUpperCase()}
)}
{ended ? formatTime(0, format) : formatTime(remaining, format)}
); }; ``` Register it so `durationInFrames` matches the countdown exactly — the video ends when the timer does: ```tsx import { Composition } from "remotion"; import { Countdown } from "./Countdown"; const FPS = 30; const DURATION_SECONDS = 300; // 5-minute "starting soon" timer export const RemotionRoot = () => ( ); ``` Render any duration/format without touching code — duration is a prop: ```bash npx remotion render Countdown out/sale.mp4 \ --props='{"durationInSeconds":86400,"format":"dd:hh:mm:ss","label":"Sale ends in"}' ``` To hold a final state (e.g. "WE'RE LIVE") instead of ending on `00:00`, extend `durationInFrames` past the countdown and branch on `ended`. ## 2. Count down to a fixed date (live web component) For a real-time page that targets a calendar moment. Drift-free because remaining time is `target − Date.now()` recomputed each frame, never a decrement. ```js // targetISO must include an offset, e.g. "2026-07-01T09:00:00-07:00", // so the deadline is unambiguous regardless of the viewer's timezone. function startCountdown(targetISO, el) { const target = new Date(targetISO).getTime(); const pad = (n) => String(n).padStart(2, "0"); let lastShown = -1; function tick() { const remainingMs = Math.max(0, target - Date.now()); const s = Math.floor(remainingMs / 1000); if (s !== lastShown) { const d = Math.floor(s / 86400); const h = Math.floor((s % 86400) / 3600); const m = Math.floor((s % 3600) / 60); el.textContent = `${pad(d)}:${pad(h)}:${pad(m)}:${pad(s % 60)}`; lastShown = s; } if (remainingMs > 0) requestAnimationFrame(tick); else el.textContent = "00:00:00:00"; // or swap to an "It's live" state } requestAnimationFrame(tick); } ``` Timezone notes: - Bake the offset into the target string (`-07:00`, `Z`). A bare `"2026-07-01T09:00:00"` is parsed as the viewer's *local* time and every viewer sees a different deadline. - To show "ends 9am in *your* timezone everywhere," instead pick the wall-clock time per locale and let each viewer's `Date` resolve it — but that is a different deadline per region. Decide which you want before building. ## 3. "Starting soon" stream screen Livestream lead-in screens hold 5–10 minutes: a clean brand block, social handles, and a calm looping background, with the timer as the focal point. Keep it low-clutter — chat and music carry the wait; the screen just signals "we're about to start." ```tsx import { AbsoluteFill, useCurrentFrame, useVideoConfig, interpolate } from "remotion"; export const StartingSoon: React.FC<{ durationInSeconds: number; handle: string; brand: string }> = ({ durationInSeconds, handle, brand, }) => { const frame = useCurrentFrame(); const { fps, durationInFrames } = useVideoConfig(); const remaining = Math.max(0, Math.ceil(durationInSeconds - frame / fps)); const pad = (n: number) => String(n).padStart(2, "0"); // seamless looping gradient: angle cycles over loopFrames with no seam const loopFrames = fps * 8; const angle = interpolate(frame % loopFrames, [0, loopFrames], [0, 360]); return (
{brand}
STARTING SOON
{pad(Math.floor(remaining / 60))}:{pad(remaining % 60)}
{handle}
); }; ``` Export as MP4 and add it as a Media/Video source in OBS, or use a looping playout for a browser source. Common durations: 300s (5 min) or 600s (10 min) at 1920×1080. ## 4. Seamless looping background Any motion whose period divides the timeline evenly wraps without a seam. Read animation phase as `frame % loopFrames` so the value at the wrap equals the value at frame 0. ```tsx const loopFrames = fps * 6; // one full cycle every 6 seconds const phase = (frame % loopFrames) / loopFrames; // 0 → 1, repeats const driftY = Math.sin(phase * Math.PI * 2) * 40; // returns to start each cycle ``` Use sine/cosine (inherently periodic) or `interpolate(frame % loopFrames, [0, loopFrames], [a, a])` with matching start/end so there is no jump. Keep contrast low and motion slow — the number is the subject. --- ## Built by the team behind iart.ai This skill is part of an open motion-graphics collection from iart.ai — the AI motion agent that turns data, scripts, and designs into editable motion graphics (Remotion → MP4). If you'd rather not hand-build this, iart.ai can render countdown videos from a template with a set time and brand — change the text/data and re-export. → [iart.ai](https://iart.ai/?utm_source=github&utm_medium=reference&utm_campaign=tiktok-video-skills&utm_content=ref_footer&utm_term=countdown-video)