useCountUp hook
Animated rolling number hook used by chart headline figures.
Installation
npx shadcn@latest add https://boardcn.dev/r/use-count-up.jsonnpx shadcn@latest add https://boardcn.dev/r/use-count-up.jsonBoardCN dependencies
The CLI installs these for you — you do not need to add them yourself.
Source
The file the CLI copies into your project.
"use client";
import { useEffect, useRef, useState } from "react";
const REDUCED_MOTION_QUERY = "(prefers-reduced-motion: reduce)";
export function usePrefersReducedMotion() {
const [reduced, setReduced] = useState(
() =>
typeof window !== "undefined" &&
typeof window.matchMedia === "function" &&
window.matchMedia(REDUCED_MOTION_QUERY).matches,
);
useEffect(() => {
if (typeof window.matchMedia !== "function") return;
const media = window.matchMedia(REDUCED_MOTION_QUERY);
const update = () => setReduced(media.matches);
update();
media.addEventListener("change", update);
return () => media.removeEventListener("change", update);
}, []);
return reduced;
}
const easeOutCubic = (t: number) => 1 - Math.pow(1 - t, 3);
/**
* Rolls the displayed number from its current value to `target` with an
* ease-out curve. Used by the dashboard chart cards so headline figures
* animate when the dataset or the hovered point changes.
*/
export function useCountUp(target: number, duration = 320) {
const [display, setDisplay] = useState(target);
const fromRef = useRef(target);
const reducedMotion = usePrefersReducedMotion();
useEffect(() => {
const from = fromRef.current;
if (from === target) return;
if (reducedMotion || duration <= 0) {
fromRef.current = target;
setDisplay(target);
return;
}
const start = performance.now();
let raf = 0;
const tick = (now: number) => {
const t = Math.min(1, (now - start) / duration);
const value = Math.round(from + (target - from) * easeOutCubic(t));
fromRef.current = value;
setDisplay(value);
if (t < 1) raf = requestAnimationFrame(tick);
else fromRef.current = target;
};
raf = requestAnimationFrame(tick);
return () => cancelAnimationFrame(raf);
}, [target, duration, reducedMotion]);
return display;
}"use client";
import { useEffect, useRef, useState } from "react";
const REDUCED_MOTION_QUERY = "(prefers-reduced-motion: reduce)";
export function usePrefersReducedMotion() {
const [reduced, setReduced] = useState(
() =>
typeof window !== "undefined" &&
typeof window.matchMedia === "function" &&
window.matchMedia(REDUCED_MOTION_QUERY).matches,
);
useEffect(() => {
if (typeof window.matchMedia !== "function") return;
const media = window.matchMedia(REDUCED_MOTION_QUERY);
const update = () => setReduced(media.matches);
update();
media.addEventListener("change", update);
return () => media.removeEventListener("change", update);
}, []);
return reduced;
}
const easeOutCubic = (t: number) => 1 - Math.pow(1 - t, 3);
/**
* Rolls the displayed number from its current value to `target` with an
* ease-out curve. Used by the dashboard chart cards so headline figures
* animate when the dataset or the hovered point changes.
*/
export function useCountUp(target: number, duration = 320) {
const [display, setDisplay] = useState(target);
const fromRef = useRef(target);
const reducedMotion = usePrefersReducedMotion();
useEffect(() => {
const from = fromRef.current;
if (from === target) return;
if (reducedMotion || duration <= 0) {
fromRef.current = target;
setDisplay(target);
return;
}
const start = performance.now();
let raf = 0;
const tick = (now: number) => {
const t = Math.min(1, (now - start) / duration);
const value = Math.round(from + (target - from) * easeOutCubic(t));
fromRef.current = value;
setDisplay(value);
if (t < 1) raf = requestAnimationFrame(tick);
else fromRef.current = target;
};
raf = requestAnimationFrame(tick);
return () => cancelAnimationFrame(raf);
}, [target, duration, reducedMotion]);
return display;
}