Count Up
moco/countupFreeComponent
Scroll-triggered number count-up with reserved width (no layout shift), built-in Intl formatters, and reduced-motion instant-set.
Live preview
live · rendered by the registry
Rendered by moco on their own page — this is the component running, not a screenshot of it.Install it
npx shadcn@latest add https://mocoui.site/r/countup.jsonSource
1"use client";23import * as React from "react";4import { useInView, useReducedMotion } from "@/components/moco/hooks";5import "./countup.css";67const groupedInt = new Intl.NumberFormat("en-US", { maximumFractionDigits: 0 });8const compactNum = new Intl.NumberFormat("en-US", {9 notation: "compact",10 maximumFractionDigits: 1,11});12const percentNum = new Intl.NumberFormat("en-US", { maximumFractionDigits: 1 });1314/**15 * Built-in formatters, named so the component works from server-rendered16 * prose where a function prop cannot cross the RSC boundary. Client callers17 * can pass `format` instead.18 */19const FORMATS = {20 /** Grouped integer: 12,345. */21 int: (n: number) => groupedInt.format(n),22 /** Currency: $ prefix, digits scale with magnitude ($1,284 / $3.50 / $0.004). */23 usd: (n: number) => {24 const d = Math.abs(n) >= 100 ? 0 : Math.abs(n) >= 1 ? 2 : 3;25 return `$${n.toLocaleString("en-US", { minimumFractionDigits: d, maximumFractionDigits: d })}`;26 },27 /** Compact: 1.2k / 3.4M. */28 compact: (n: number) => compactNum.format(n).replace("K", "k"),29 /** Percent of a 0–100 value: 42% / 99.9%. */30 percent: (n: number) => `${percentNum.format(n)}%`,31} as const;3233export type CountUpFormat = keyof typeof FORMATS;3435export type CountUpProps = {36 value: number;37 from?: number;38 /** Animation length in ms. */39 duration?: number;40 /** Built-in formatter name; ignored when `format` is given. */41 as?: CountUpFormat;42 /** Custom formatter — client-side callers only (functions cannot cross the RSC boundary). */43 format?: (n: number) => string;44 className?: string;45};4647/**48 * Counts up once, the first time it scrolls into view. Width is reserved from49 * the final value up front so nothing reflows. Disabled under reduced motion.50 */51export function CountUp({52 value,53 from = 0,54 duration = 900,55 as = "int",56 format,57 className,58}: CountUpProps) {59 const fmt = format ?? FORMATS[as];60 const ref = React.useRef<HTMLSpanElement>(null);61 const seen = useInView(ref);62 const reduced = useReducedMotion();63 const [n, setN] = React.useState(value);64 const started = React.useRef(false);6566 React.useEffect(() => {67 if (!seen || reduced || started.current) return;68 started.current = true;69 setN(from);70 const t0 = performance.now();71 let raf = 0;72 const tick = (t: number) => {73 const k = Math.min(1, (t - t0) / duration);74 const eased = 1 - Math.pow(1 - k, 3);75 setN(from + (value - from) * eased);76// … truncated
What it pulls in
Other registry items
Files it writes
More from moco
All 20 items| Component | Registry | Kind | Access | Installs | Command |
|---|---|---|---|---|---|
| Codecode | moco | Components | Free | 1 dep · 3 files | |
| Comparecompare | moco | Components | Free | no deps · 2 files | |
| Covercover | moco | Components | Free | no deps · 2 files | |
| Diagram Kitdiagram | moco | Components | Free | no deps · 2 files | |
| Dot Griddotgrid | moco | Components | Free | no deps · 2 files | |
| Figurefigure | moco | Components | Free | no deps · 2 files | |
| Hookshooks | moco | Components | Free | no deps | |
| Islandisland | moco | Components | Free | no deps · 2 files |
