# Count Up

> In-view count-up number with reduced-motion support.

A shadcn/ui block from the [@atroui](https://www.atroui.com) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/atroui/count-up).

- Page, with a live preview: https://shadcn.exchange/r/atroui/count-up
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/atroui/count-up-light-7fe3442332c423e5.png
- Registry item: https://www.atroui.com/r/count-up.json
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://www.atroui.com/r/count-up.json
```

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

## Files

### `registry/default/blocks/count-up.tsx`

```tsx
"use client"

import * as React from "react"

/**
 * Counts up from 0 to `value` over `duration` ms with out-quart easing.
 * Runs once on first in-view and respects prefers-reduced-motion.
 */
export function CountUp({
  value,
  duration = 1200,
  className,
  ariaLabel,
}: {
  value: number
  duration?: number
  className?: string
  ariaLabel?: string
}) {
  const ref = React.useRef<HTMLSpanElement | null>(null)
  const [display, setDisplay] = React.useState(0)
  const startedRef = React.useRef(false)

  React.useEffect(() => {
    const node = ref.current
    if (!node) return

    const reduced =
      window.matchMedia?.("(prefers-reduced-motion: reduce)").matches
    if (reduced) {
      setDisplay(value)
      return
    }

    let raf = 0
    const start = () => {
      if (startedRef.current) return
      startedRef.current = true
      const t0 = performance.now()
      const from = 0
      const to = value
      const ease = (t: number) => 1 - Math.pow(1 - t, 4)

      const step = (now: number) => {
        const t = Math.min(1, (now - t0) / duration)
        setDisplay(Math.round(from + (to - from) * ease(t)))
        if (t < 1) raf = requestAnimationFrame(step)
      }
      raf = requestAnimationFrame(step)
    }

    if (typeof IntersectionObserver === "undefined") {
      start()
      return () => cancelAnimationFrame(raf)
    }

    const io = new IntersectionObserver(
      (entries) => {
        if (entries[0]?.isIntersecting) {
          start()
          io.disconnect()
        }
      },
      { threshold: 0.3 }
    )
    io.observe(node)
    return () => {
      io.disconnect()
      cancelAnimationFrame(raf)
    }
  }, [value, duration])

  return (
    <span
      ref={ref}
      className={className}
      aria-label={ariaLabel ?? String(value)}
    >
      {display}
    </span>
  )
}
```
