npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

solid-countup

v1.0.1

Published

A Solid component that animates a number from one value to another — a take on CountUp.js that renders declaratively, follows props reactively, and formats through Intl.NumberFormat.

Downloads

519

Readme

solid-countup

pipeline status license: MIT TypeScript strict maintained with pnpm

A Solid component that animates a number from one value to another — a take on CountUp.js 2.10.1 that renders declaratively, follows your props reactively, and formats through Intl.NumberFormat.

import CountUp from 'solid-countup'

<CountUp endVal={5234} autoAnimate />

starts at 0 and counts up to 5,234 once it scrolls into view.

Install

npm install solid-countup
# or
pnpm add solid-countup
# or
yarn add solid-countup

solid-js is a peer dependency (^1.6.0) and is never bundled.

Quick start

CountUp renders its startVal and holds it until the animation is started. Start it when it scrolls into view:

import { render } from 'solid-js/web'
import CountUp from 'solid-countup'

render(() => <CountUp endVal={5234} autoAnimate />, document.body)

or start it yourself through the control handle:

import { CountUp, type CountUpHandle } from 'solid-countup'

let handle: CountUpHandle | undefined

<CountUp endVal={5234} ref={h => (handle = h)} />
<button onClick={() => handle?.start()}>Start</button>

Either way, it animates from 0 to 5,234 over 2 seconds and leaves the final value in the DOM.

What the output looks like

Each row shows the value after the animation has run to completion. Output assumes an en-US runtime locale; pass locale to pin it. (Currency is the one that diverges visibly — under en-CA the same example renders US$1,234.50.)

| Code | Renders | | --- | --- | | <CountUp endVal={5234} /> | 5,234 | | <CountUp endVal={5234} duration={0} /> | 5,234 (no animation) | | <CountUp startVal={100} endVal={0} /> | 0 (counts down) | | <CountUp endVal={5234} prefix="$" suffix=" USD" /> | $5,234 USD | | <CountUp endVal={0.856} decimalPlaces={2} intlNumberFormatConfig={{ style: 'percent' }} /> | 85.60% | | <CountUp endVal={1234.5} decimalPlaces={2} locale="en-US" intlNumberFormatConfig={{ style: 'currency', currency: 'USD' }} /> | $1,234.50 | | <CountUp endVal={1_200_000} decimalPlaces={1} locale="en-US" intlNumberFormatConfig={{ notation: 'compact' }} /> | 1.2M | | <CountUp endVal={1234567} locale="en-IN" /> | 12,34,567 (Indian grouping) | | <CountUp endVal={1234567} locale="hi-IN-u-nu-deva" /> | १२,३४,५६७ (Devanagari digits) | | <CountUp endVal={1234567} separator=" " decimal="," /> | 1 234 567 (custom glyphs) |

Props

CountUp renders a <span> and spreads the rest of its props onto that element, so class, id, style, role, aria-* and every other <span> attribute work as usual.

| Prop | Type | Default | Notes | | --- | --- | --- | --- | | endVal | number | — | Required. The target value. | | startVal | number | 0 | The value the animation starts from. | | duration | number | 2 | Duration in seconds. 0 prints the final value immediately. | | decimalPlaces | number | 0 | Fraction digits, clamped to 0–100 — or the runtime's lower cap (20 on older engines). Drives Intl's min/max fraction digits. | | useEasing | boolean | true | Set false for a linear count. | | easingFn | (progress: number) => number | log-scaled ease-out | Receives normalised progress in [0, 1] and returns eased progress. Not Penner (t, b, c, d). | | useGrouping | boolean | true | Thousands separators. | | locale | string | runtime default | BCP 47 locale for Intl.NumberFormat. Reactive. | | prefix | string | '' | Literal text before the number. | | suffix | string | '' | Literal text after the number. | | separator | string | locale's group char | Override the grouping glyph. | | decimal | string | locale's decimal char | Override the decimal glyph. | | formattingFn | (value, prefix, suffix) => JSXElement | Intl-based | Full output override. Receives the raw number. | | ref | (handle: CountUpHandle) => void | — | Called with the control handle. | | onStart | () => void | — | Fires when the animation starts. | | onComplete | (endVal: number) => void | — | Fires when the animation reaches the target. | | onFrame | (value: number, progress: number) => void | — | Fires every frame, including the last. | | autoAnimate | boolean | false | Start when scrolled into view (IntersectionObserver). | | autoAnimateThreshold | number | 0 | IntersectionObserver threshold. | | autoAnimateDelay | number | 200 | Milliseconds to wait after entering view before starting. | | autoAnimateOnce | boolean | false | Animate only the first time it enters view. | | reducedMotion | 'user' \| 'always' \| 'never' | 'user' | See Reduced motion. | | intlNumberFormatConfig | Intl.NumberFormatOptions | {} | Passed to the Intl.NumberFormat constructor. |

decimalPlaces, useGrouping and intlNumberFormatConfig are assigned after the spread inside the constructor, so decimalPlaces wins over any minimumFractionDigits / maximumFractionDigits you set, and useGrouping wins over intlNumberFormatConfig.useGrouping. That is why the currency example above passes decimalPlaces={2} to get cents.

Control handle

Pass a ref to receive an object with imperative controls. Every method is stable for the life of the component.

import { CountUp, type CountUpHandle } from 'solid-countup'

let handle: CountUpHandle | undefined

<CountUp endVal={5234} ref={h => (handle = h)} />
<button onClick={() => handle?.start()}>Start</button>

| Method | Description | | --- | --- | | start() | Reset and animate to endVal. | | pause() | Stop requesting frames, holding the current value. | | resume() | Continue from where it paused. | | toggle() | Pause if running, resume if paused. | | reset() | Return to startVal and cancel any pending frame or delayed start. | | update(value) | Animate from the current position to a new target. |

App-wide defaults

Wrap a subtree in CountUpDefaults to configure many counters at once. Values are merged with the built-in defaults, and a prop on the individual <CountUp> still wins.

import { CountUp, CountUpDefaults } from 'solid-countup'

<CountUpDefaults value={{ duration: 1.2, suffix: '%' }}>
  <div class="stats">
    <CountUp endVal={42} />
    <CountUp endVal={87} />
  </div>
</CountUpDefaults>

Providers nest; an inner CountUpDefaults merges over an outer one.

Recipes

Currency and percent

<CountUp endVal={1234.5} decimalPlaces={2} locale="en-US" intlNumberFormatConfig={{ style: 'currency', currency: 'USD' }} />
// $1,234.50

<CountUp endVal={0.856} decimalPlaces={2} intlNumberFormatConfig={{ style: 'percent' }} />
// 85.60%

Compact and signed numbers

<CountUp endVal={1_200_000} decimalPlaces={1} locale="en-US" intlNumberFormatConfig={{ notation: 'compact' }} />
// 1.2M

<CountUp endVal={1234} locale="en-US" intlNumberFormatConfig={{ signDisplay: 'always' }} />
// +1,234

Custom glyphs

separator and decimal override what Intl produces, without changing the locale:

<CountUp endVal={1234567.89} decimalPlaces={2} locale="en-US" separator=" " decimal="," />
// 1 234 567,89

Split digits (custom rendering)

formattingFn gets the raw number and returns a JSXElement, so you can emit any structure — one element per digit, an odometer scaffold, a styled component:

import { For } from 'solid-js'

<CountUp
  endVal={5234}
  formattingFn={value => (
    <span class="digits">
      <For each={new Intl.NumberFormat().format(value).split('')}>
        {char => <span class="digit">{char}</span>}
      </For>
    </span>
  )}
/>

prefix and suffix live inside the default formatter, so a custom formattingFn replaces them too. Format the value yourself and prepend/append whatever you need.

Server-side rendering

On the server the component renders the final value, so the first paint never shows 0 and hydration cannot disagree:

import { renderToString } from 'solid-js/web'
import CountUp from 'solid-countup'

renderToString(() => <CountUp endVal={5234} />)
// '<span aria-live="off">5,234</span>'

Nothing touches window, document, requestAnimationFrame, IntersectionObserver or matchMedia during SSR. The first client render matches the server output, so hydration cannot disagree. onMount then rewinds the display to startVal (unless motion is skipped), ready for the animation to be started.

Accessibility

The animated <span> carries aria-live="off", because a live region here would queue roughly 120 announcements per second and drown out the rest of the page. role="status" implies aria-live="polite", so if you add one you almost certainly want to re-assert aria-live="off":

<CountUp endVal={5234} role="status" aria-live="off" />

aria-live is placed before the spread, so an explicit value from the caller always wins.

Reduced motion

By default the component respects prefers-reduced-motion: reduce. When motion is skipped, start(), resume() and update() jump straight to the target and fire onComplete — no frame is ever scheduled, and onStart never fires.

<CountUp endVal={5234} />                      // respect the OS setting ('user')
<CountUp endVal={5234} reducedMotion="always" />  // always skip the animation
<CountUp endVal={5234} reducedMotion="never" />   // always animate

Reactive endVal

Changing endVal animates from the current position to the new target, with no imperative call:

import { createSignal } from 'solid-js'

const [value, setValue] = createSignal(0)

<CountUp endVal={value()} />

setValue(5234) // animates from wherever the display currently is

The update fires on change only. Don't mix this with update() on the same instance — update() moves the display off the prop value, and re-asserting the same endVal won't resync it.

CommonJS

The package ships both ESM and CommonJS. Because it exposes a named CountUp and a default export, require consumers need .default (or the named binding):

const CountUp = require('solid-countup').default
// or
const { CountUp } = require('solid-countup')

Browser support

Any browser with Intl.NumberFormat and requestAnimationFrame. IntersectionObserver is only needed when autoAnimate is enabled, and matchMedia only for reducedMotion: 'user'. Internet Explorer is not supported.

Why not CountUp.js directly?

solid-countup is a reimplementation from CountUp.js's documented behaviour, not a wrapper.

| | solid-countup | CountUp.js 2.10.1 | | --- | --- | --- | | Usage | Declarative <CountUp> component | Imperative, you own the element | | Reactivity | endVal is a reactive prop; shared defaults via context | Re-run update() yourself | | Easing contract | (progress: number) => number, normalised to [0, 1] | Penner (t, b, c, d) | | Per-frame callback | onFrame(value, progress) every frame | none | | Formatting | Intl.NumberFormat — locale, currency, percent, compact, RTL and non-Latin digits | custom chars and regex | | Reduced motion | prefers-reduced-motion respected by default | opt-in | | Teardown | automatic on unmount | manual | | SSR | renders the final value | not applicable |

Contributing

See CONTRIBUTING.md.

License and credits

MIT.

Built on the idea behind CountUp.js by Jamie Perkins. It is an independent reimplementation from CountUp.js's documented behaviour, not a fork or a wrapper, and it borrows option names only where they fit — the animation model, easing contract, formatting and several defaults are this project's own. See Why not CountUp.js directly?.