{
  "slug": "stat-counter",
  "title": "Stat counter",
  "tagline": "A big number counts up from zero (pure CSS @property animation, so it seeks perfectly), with label and delta pill. 4 s, 16:9.",
  "description": "Free CC0 animated statistic motion template: number count-up driven by a registered CSS property, label, delta badge, grid backdrop. Self-contained HTML, deterministic, agent-friendly.",
  "category": "chart",
  "tags": [
    "number",
    "counter",
    "kpi",
    "stat",
    "data"
  ],
  "tier": "free",
  "license": "CC0",
  "spec": {
    "width": 1920,
    "height": 1080,
    "fps": 30,
    "duration": 4,
    "aspect": "16:9"
  },
  "kind": "html",
  "params_schema": {
    "type": "object",
    "properties": {
      "value": {
        "type": "integer",
        "minimum": 0,
        "maximum": 999999999,
        "default": 12500,
        "description": "Final integer value (no thousands separators — add them via suffix/prefix text if needed)"
      },
      "prefix": {
        "type": "string",
        "default": "",
        "maxLength": 6,
        "description": "Text before the number ($, €, ~)"
      },
      "suffix": {
        "type": "string",
        "default": "+",
        "maxLength": 6,
        "description": "Text after the number (+, %, k, M)"
      },
      "label": {
        "type": "string",
        "default": "Templates rendered",
        "maxLength": 40,
        "description": "Uppercase label under the number"
      },
      "delta": {
        "type": "string",
        "default": "▲ 38% this month",
        "maxLength": 32,
        "description": "Small pill (empty string hides it)"
      },
      "accent": {
        "type": "string",
        "format": "color",
        "default": "#22c55e",
        "description": "Pill, underline colour"
      },
      "bg": {
        "type": "string",
        "format": "color",
        "default": "#0b0c10",
        "description": "Background"
      },
      "countSeconds": {
        "type": "number",
        "minimum": 0.3,
        "maximum": 3.2,
        "default": 2.2,
        "description": "How long the count-up takes"
      }
    }
  },
  "default_params": {
    "value": 12500,
    "prefix": "",
    "suffix": "+",
    "label": "Templates rendered",
    "delta": "▲ 38% this month",
    "accent": "#22c55e",
    "bg": "#0b0c10",
    "countSeconds": 2.2
  },
  "preview": {
    "poster": "https://gimgs.net/motion/media/stat-counter/poster-0403d1d1f280.jpg",
    "video": ""
  },
  "variants": [],
  "credits": {
    "author": "gimgs",
    "url": "https://gimgs.net/motion"
  },
  "page_url": "https://gimgs.net/motion/stat-counter.html",
  "json_url": "https://gimgs.net/motion/stat-counter.json",
  "template_url": "https://gimgs.net/motion/stat-counter/template.html",
  "play_url": "https://gimgs.net/motion/stat-counter/play",
  "markdown_url": "https://gimgs.net/motion/stat-counter.md",
  "updated_at": "2026-09-24T07:24:41.486Z",
  "prompt": "## When to use\nOne KPI on screen: users, revenue, downloads, savings. Reports, pitch videos, year-in-review, product updates.\n\n## How to adapt\n- Big numbers read better scaled: `value: 12, suffix: \"M\"` instead of `12000000`.\n- `prefix`/`suffix` are rendered through CSS `content`, so keep them short and plain (currency symbols and % are fine).\n- The count-up is a CSS animation on a registered custom property (`@property --n`), which is why any frame `t` renders the exact same digits — needs Chromium ≥ 85 (Playwright/Chrome headless are fine).\n- Timeline: grid fade 0–1 s, pop 0.2–0.8 s, count 0.3 → 0.3 + `countSeconds`, label 1.2–1.8 s, delta 1.8–2.4 s, underline 0.4–2.0 s.\n- Sequence several KPIs by rendering one clip per stat with the same `accent`/`bg`.\n\n## Do not change\n`window.motion`, `#stage` size, `@property --n`, `animation-fill-mode: both`.",
  "flow": [
    {
      "step": "Pick the stat",
      "note": "Round it; scale with suffix (k/M)."
    },
    {
      "step": "Render",
      "cmd": "frames 0…119 at t=i/30 → ffmpeg -framerate 30 -i frame-%04d.png -c:v libx264 -pix_fmt yuv420p stat.mp4"
    }
  ],
  "files": [],
  "contract": "Self-contained HTML. Declares window.motion = { width, height, fps, duration, params, apply(params) [, seek(t)] }; root element #stage sized width×height; animations are CSS/WAAPI with animation-fill-mode: both so any t can be seeked via document.getAnimations(). The host runtime (injected by /play) merges ?p params, calls apply(), scales #stage to the viewport and, when ?t is given, pauses and seeks deterministically.",
  "render": {
    "frames": 120,
    "frame_url": "https://gimgs.net/motion/stat-counter/play?p=<base64url(JSON params)>&t=<seconds>",
    "note": "Open frame_url in a headless browser with viewport width×height (deviceScaleFactor 1), wait for document.documentElement.dataset.motionReady === '1', screenshot; repeat for t = i/fps (i = 0…frames-1); then `ffmpeg -framerate <fps> -i frame-%04d.png -c:v libx264 -pix_fmt yuv420p out.mp4`. Or drop ?t to play it live."
  },
  "source": "<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<title>Stat counter</title>\n<style>\n  @font-face{font-family:'Bricolage Grotesque';font-weight:600 800;font-display:block;src:url(https://gimgs.net/fonts/bricolage-600-latin.woff2) format('woff2')}\n  @font-face{font-family:'IBM Plex Sans';font-weight:400 600;font-display:block;src:url(https://gimgs.net/fonts/plexsans-400-latin.woff2) format('woff2')}\n  /* registered custom property → the number itself is a CSS animation, so it seeks like everything else */\n  @property --n { syntax: '<integer>'; inherits: false; initial-value: 0; }\n  html,body{margin:0;background:#000}\n  #stage{position:relative;width:1920px;height:1080px;overflow:hidden;background:var(--bg,#0b0c10);color:#fff;font-family:'IBM Plex Sans',system-ui,sans-serif}\n  .grid{position:absolute;inset:0;background-image:linear-gradient(rgba(255,255,255,.05) 1px,transparent 1px),linear-gradient(90deg,rgba(255,255,255,.05) 1px,transparent 1px);background-size:80px 80px;\n    mask-image:radial-gradient(60% 60% at 50% 50%,#000 30%,transparent 100%);opacity:0;animation:fade 1s both}\n  @keyframes fade{to{opacity:1}}\n  .box{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);text-align:center}\n  .num{font-family:'Bricolage Grotesque',Inter,system-ui,sans-serif;font-weight:800;font-size:300px;line-height:1;letter-spacing:-.05em;\n    --n:0;counter-reset:n var(--n);animation:count var(--dur,2.2s) .3s cubic-bezier(.2,.8,.2,1) both, pop .6s .2s cubic-bezier(.2,.9,.2,1.1) both}\n  .num::before{content:var(--prefix,\"\")}\n  .num::after{content:counter(n) var(--suffix,\"\")}\n  .num .accent{color:var(--accent,#22c55e)}\n  @keyframes count{from{--n:0}to{--n:var(--target,0)}}\n  @keyframes pop{from{transform:scale(.7);opacity:0}to{transform:none;opacity:1}}\n  .label{margin-top:26px;font-size:44px;font-weight:500;letter-spacing:.18em;text-transform:uppercase;color:rgba(255,255,255,.6);\n    opacity:0;transform:translateY(20px);animation:up .6s 1.2s cubic-bezier(.2,.8,.2,1) both}\n  .delta{display:inline-block;margin-top:30px;padding:12px 30px;border-radius:999px;background:color-mix(in srgb,var(--accent,#22c55e) 18%,transparent);color:var(--accent,#22c55e);font-size:40px;font-weight:600;\n    opacity:0;transform:translateY(20px);animation:up .6s 1.8s cubic-bezier(.2,.8,.2,1) both}\n  .delta:empty{display:none}\n  @keyframes up{to{transform:none;opacity:1}}\n  .line{position:absolute;left:50%;bottom:180px;height:4px;width:0;background:var(--accent,#22c55e);transform:translateX(-50%);animation:line 1.6s .4s cubic-bezier(.2,.8,.2,1) both}\n  @keyframes line{to{width:520px}}\n</style>\n</head>\n<body>\n<div id=\"stage\">\n  <div class=\"grid\"></div>\n  <div class=\"box\"><div class=\"num\" id=\"num\"></div><div class=\"label\" id=\"label\"></div><div class=\"delta\" id=\"delta\"></div></div>\n  <div class=\"line\"></div>\n</div>\n<script>\nwindow.motion = {\n  width: 1920, height: 1080, fps: 30, duration: 4,\n  params: { value: 12500, prefix: \"\", suffix: \"+\", label: \"Templates rendered\", delta: \"▲ 38% this month\", accent: \"#22c55e\", bg: \"#0b0c10\", countSeconds: 2.2 },\n  apply: function (p) {\n    var s = document.getElementById(\"stage\");\n    s.style.setProperty(\"--accent\", p.accent); s.style.setProperty(\"--bg\", p.bg);\n    var num = document.getElementById(\"num\");\n    var target = Math.max(0, Math.round(+p.value || 0));\n    num.style.setProperty(\"--target\", String(target));\n    num.style.setProperty(\"--dur\", Math.max(0.3, Math.min(3.2, +p.countSeconds || 2.2)) + \"s\");\n    // prefix/suffix go through CSS strings; quote them safely\n    num.style.setProperty(\"--prefix\", JSON.stringify(String(p.prefix || \"\")));\n    num.style.setProperty(\"--suffix\", JSON.stringify(String(p.suffix || \"\")));\n    document.getElementById(\"label\").textContent = p.label;\n    document.getElementById(\"delta\").textContent = p.delta || \"\";\n  }\n};\n</script>\n</body>\n</html>\n"
}