# Odometer: Landing Stats

> A band of headline numbers that roll up from 0 the first time they scroll into view, one after another.

A shadcn/ui block from the [@keyframeui](https://keyframeui.com) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/keyframeui/odometer-stats-demo).

- Page, with a live preview: https://shadcn.exchange/r/keyframeui/odometer-stats-demo
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/keyframeui/odometer-stats-demo-light-c6d4f385cb31ebaa.png
- Registry item: https://keyframeui.com/r/odometer-stats-demo.json
- Categories: number, data, motion
- Listed: 2026-10-14, source last changed 2026-10-15

## Install

```bash
npx shadcn@latest add https://keyframeui.com/r/odometer-stats-demo.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- Registry items: `https://keyframeui.com/r/odometer.json`

## Notes from the registry

Live preview, examples and props: https://keyframeui.com/components/odometer#odometer-stats-demo

## Files

### `registry/demos/odometer-stats-demo.tsx`

```tsx
"use client";

import * as React from "react";

import { Odometer } from "@/components/ui/odometer";

const stats: {
  value: number;
  format?: Intl.NumberFormatOptions;
  suffix?: string;
  label: string;
}[] = [
  {
    value: 2_400_000,
    format: { notation: "compact", maximumFractionDigits: 1 },
    label: "Downloads",
  },
  { value: 180, suffix: "+", label: "Countries" },
  {
    value: 0.9999,
    format: { style: "percent", minimumFractionDigits: 2 },
    label: "Uptime, last 12 months",
  },
  {
    value: 4.9,
    format: { minimumFractionDigits: 1 },
    suffix: "/5",
    label: "Average rating",
  },
];

// Each number waits on 0 until the band scrolls into view, then rolls up,
// a beat after the one before it. Replay remounts the band to roll again.
export function OdometerStatsDemo() {
  const [run, setRun] = React.useState(0);

  return (
    <section className="@container w-full max-w-4xl rounded-2xl border border-zinc-200 bg-white p-6 shadow-sm shadow-zinc-950/[0.03] sm:p-8 dark:border-zinc-800 dark:bg-zinc-900 dark:shadow-none">
      <div className="flex items-end justify-between gap-6">
        <div>
          <p className="font-mono text-xs tracking-widest text-zinc-500 uppercase">By the numbers</p>
          <h3 className="mt-2 text-xl font-semibold tracking-tight text-balance text-zinc-950 dark:text-zinc-50">
            Shipped by teams in every time zone.
          </h3>
        </div>
        <button
          type="button"
          onClick={() => setRun((r) => r + 1)}
          className="inline-flex h-8 shrink-0 items-center gap-1.5 rounded-lg border border-zinc-200 px-2.5 text-xs font-medium text-zinc-600 transition-[background-color,color,scale] duration-150 outline-none hover:bg-zinc-100 hover:text-zinc-950 focus-visible:ring-2 focus-visible:ring-zinc-400 active:scale-[0.96] dark:border-zinc-700 dark:text-zinc-400 dark:hover:bg-zinc-800 dark:hover:text-zinc-50"
        >
          <svg
            aria-hidden
            viewBox="0 0 16 16"
            fill="none"
            stroke="currentColor"
            strokeWidth="1.5"
            strokeLinecap="round"
            strokeLinejoin="round"
            className="size-3.5 transition-[rotate] duration-500 ease-[cubic-bezier(0.22,1,0.36,1)]"
            style={{ rotate: `${run * 360}deg` }}
          >
            <path d="M13.5 8a5.5 5.5 0 1 1-1.6-3.9" />
            <path d="M13.5 2.5v3h-3" />
          </svg>
          Replay
        </button>
      </div>

      <dl
        key={run}
        className="mt-8 grid grid-cols-2 gap-y-8 border-t border-zinc-200 pt-8 @2xl:grid-cols-4 dark:border-zinc-800"
      >
        {stats.map((stat, i) => (
          <div
            key={stat.label}
            className="flex flex-col-reverse gap-2 border-zinc-200 @2xl:border-l @2xl:px-6 @2xl:first:border-l-0 @2xl:first:pl-0 dark:border-zinc-800"
          >
            <dt className="text-sm text-zinc-500 dark:text-zinc-400">{stat.label}</dt>
            <dd className="text-[clamp(2.25rem,7cqw,3.25rem)] leading-none font-semibold tracking-[-0.045em] text-zinc-950 dark:text-zinc-50">
              <Odometer value={stat.value} format={stat.format} rollOnView delay={i * 0.12} />
              {stat.suffix ? <span className="text-zinc-400 dark:text-zinc-500">{stat.suffix}</span> : null}
            </dd>
          </div>
        ))}
      </dl>
    </section>
  );
}
```
