Count Up
loomui/count-upFreeComponent
A number that counts to its value when it scrolls into view, without a render per frame.
Live preview
live · rendered by the registry
Rendered by loomui on their own page — this is the component running, not a screenshot of it.Install it
npx shadcn@latest add https://loomui.design/r/count-up.jsonSource
1"use client"23import * as React from "react"45import { cn } from "@/lib/utils"67export interface CountUpProps extends Omit<8 React.ComponentProps<"span">,9 "children"10> {11 /** The number to land on. */12 value: number13 /** The number to start from. */14 from?: number15 /** Milliseconds for the whole count. */16 duration?: number17 /** Decimal places to hold throughout the count. */18 decimals?: number19 /** Locale passed to Intl.NumberFormat, so grouping matches the reader. */20 locale?: string21 /** Rendered before the number, inside the same element. */22 prefix?: string23 /** Rendered after the number. */24 suffix?: string25 /** Hold until the number scrolls into view instead of counting on mount. */26 startOnView?: boolean27 /** Render the final value with no count. */28 disabled?: boolean29}3031/** --ease-out-quart, expressed as a function so JS and CSS agree. */32function easeOutQuart(t: number) {33 return 1 - Math.pow(1 - t, 4)34}3536export function CountUp({37 value,38 from = 0,39 duration = 1400,40 decimals = 0,41 locale,42 prefix,43 suffix,44 startOnView = true,45 disabled = false,46 className,47 ...props48}: CountUpProps) {49 const ref = React.useRef<HTMLSpanElement>(null)50 const [active, setActive] = React.useState(!startOnView)5152 const format = React.useMemo(53 () =>54 new Intl.NumberFormat(locale, {55 minimumFractionDigits: decimals,56 maximumFractionDigits: decimals,57 }),58 [locale, decimals]59 )6061 React.useEffect(() => {62 if (!startOnView || disabled) {63 return64 }6566 const node = ref.current67 if (!node || typeof IntersectionObserver === "undefined") {68 setActive(true)69 return70 }7172 const observer = new IntersectionObserver(73 ([entry]) => {74 if (entry.isIntersecting) {75 setActive(true)76 observer.disconnect()77 }78 },79 { threshold: 0.4 }80 )8182 observer.observe(node)83 return () => observer.disconnect()84 }, [startOnView, disabled])8586 React.useEffect(() => {87 const node = ref.current88 if (!node) {89 return90 }9192 const reduced =93 typeof window !== "undefined" &&94 window.matchMedia("(prefers-reduced-motion: reduce)").matches9596 if (disabled || !active || reduced || duration <= 0) {97 node.textContent = format.format(value)98 return99 }100101 let frame = 0102 let start: number | null = null103104 // The value is written straight to the DOM rather than through state, so a105// … truncated
What it pulls in
Other registry items
Files it writes
More from loomui
All 91 items| Component | Registry | Kind | Access | Installs | Command |
|---|---|---|---|---|---|
| Aurora Backdropaurora-backdrop | loomui | Components | Free | no deps | |
| Bento Gridbento-grid | loomui | Components | Free | no deps | |
| Card Stackcard-stack | loomui | Components | Free | no deps | |
| Compare Slidercompare-slider | loomui | Components | Free | no deps | |
| Confetti Buttonconfetti-button | loomui | Components | Free | no deps | |
| Credit Cardcredit-card | loomui | Components | Free | no deps | |
| Drawerdrawer | loomui | Components | Free | 1 dep | |
| Elastic Tabselastic-tabs | loomui | Components | Free | no deps |
