react-countup vs react-spring
Animating Numbers vs General Motion in React
react-countupreact-springSimilar Packages:

Animating Numbers vs General Motion in React

react-countup and react-spring are both animation libraries for React, but they solve different problems. react-countup is a specialized wrapper designed specifically for animating numeric values from a start point to an end point, often used for statistics or counters. react-spring is a general-purpose physics-based animation library that handles complex motion, transitions, and interactions across any CSS property or value. While react-countup focuses on ease of use for numbers, react-spring provides deep control over the animation lifecycle and physical behavior.

Npm Package Weekly Downloads Trend

3 Years

Github Stars Ranking

Stat Detail

Package
Downloads
Stars
Size
Issues
Publish
License
react-countup02,07433.5 kB323 years agoMIT
react-spring029,1657.23 kB795 months agoMIT

Animating Numbers vs General Motion in React

Both react-countup and react-spring bring motion to React applications, but they target different layers of the UI experience. react-countup is a specialist tool for counting numbers, while react-spring is a general engine for physical motion. Understanding their core differences helps you avoid over-engineering simple tasks or under-engineering complex interactions.

🎯 Core Purpose: Specialized Counters vs Physics Engine

react-countup is built specifically to animate numbers from a start value to an end value.

  • It wraps the underlying countUp.js library to make it React-friendly.
  • It handles the math, timing, and formatting for you.
// react-countup: Dedicated number animation
import CountUp from 'react-countup';

function StatCard() {
  return <CountUp end={1000} duration={2.5} separator="," />;
}

react-spring is a physics-based animation library for any animatable value.

  • It uses spring physics to calculate movement rather than fixed durations.
  • You can animate numbers, but also positions, opacity, scales, and colors.
// react-spring: General physics animation
import { useSpring, animated } from '@react-spring/web';

function StatCard() {
  const props = useSpring({ to: { val: 1000 }, from: { val: 0 } });
  return <animated.span>{props.val.to(n => n.toFixed(0))}</animated.span>;
}

βš™οΈ Implementation: Props vs Hooks

react-countup offers both a component and a hook for flexibility.

  • The component is declarative and works well for static layouts.
  • The hook gives you control over the ref and manual start/stop actions.
// react-countup: Component vs Hook
import CountUp, { useCountUp } from 'react-countup';

// Component approach
<CountUp end={500} />

// Hook approach
function ManualCounter() {
  const { countUpRef, start } = useCountUp({ end: 500 });
  return <button onClick={start}><span ref={countUpRef} /></button>;
}

react-spring relies heavily on hooks to manage animation state.

  • You define the spring configuration inside the component body.
  • The returned values are animated objects that bind to DOM elements.
// react-spring: Hook-based configuration
import { useSpring } from '@react-spring/web';

function InteractiveCounter() {
  const [toggle, setToggle] = React.useState(false);
  const props = useSpring({ 
    to: { val: toggle ? 100 : 0 }, 
    from: { val: 0 } 
  });
  return <button onClick={() => setToggle(!toggle)}>Toggle</button>;
}

πŸ”’ Formatting and Localization

react-countup has built-in support for formatting numbers.

  • You can add prefixes, suffixes, and decimal separators directly via props.
  • This saves you from writing custom interpolation logic for currency or stats.
// react-countup: Built-in formatting
<CountUp 
  end={1234.56} 
  prefix="$" 
  suffix=" USD" 
  decimals={2} 
  separator="," 
/>
// Output: $1,234.56 USD

react-spring requires manual formatting for text content.

  • The animated value is a number, so you must convert it to a string.
  • You need to write your own logic for currency symbols or commas.
// react-spring: Manual formatting
import { useSpring, animated } from '@react-spring/web';

const props = useSpring({ to: { val: 1234.56 }, from: { val: 0 } });

return (
  <animated.span>
    ${props.val.to(n => n.toFixed(2).replace(/\d(?=(\d{3})+\.)/g, '$&,'))} USD
  </animated.span>
);

🌊 Physics and Control

react-countup uses standard easing functions over a fixed duration.

  • You set a duration in seconds and an easing function string.
  • It is predictable but lacks physical realism like bounce or tension.
// react-countup: Fixed duration easing
<CountUp 
  end={100} 
  duration={3} 
  easingFn={(t, b, c, d) => t / d * c + b} 
/>

react-spring uses mass, tension, and friction to define motion.

  • Animations feel more natural because they mimic real-world physics.
  • You can interrupt animations mid-flight without breaking the flow.
// react-spring: Physics configuration
const props = useSpring({ 
  to: { val: 100 }, 
  from: { val: 0 }, 
  config: { mass: 1, tension: 170, friction: 26 } 
});

🧩 Integration with Layout

react-countup is isolated to the text content it renders.

  • It does not affect layout properties like width, height, or position.
  • You must wrap it in other tools if you need the container to animate too.
// react-countup: Text content only
<div className="card">
  <CountUp end={100} /> {/* Only number animates */}
</div>

react-spring can animate the number and the layout simultaneously.

  • You can bind the same spring to opacity, scale, and the number value.
  • This creates cohesive motion where elements grow and count up together.
// react-spring: Unified motion
const props = useSpring({ 
  to: { val: 100, opacity: 1, scale: 1 }, 
  from: { val: 0, opacity: 0, scale: 0.5 } 
});

return (
  <animated.div style={{ opacity: props.opacity, scale: props.scale }}>
    {props.val.to(n => n.toFixed(0))}
  </animated.div>
);

πŸ“Š Summary: Key Differences

Featurereact-countupreact-spring
Primary UseπŸ”’ Number counters🎨 General motion & physics
Setupβœ… Props-based (Component or Hook)πŸͺ Hook-based configuration
FormattingπŸ› οΈ Built-in (prefix, suffix, etc.)✍️ Manual interpolation required
Motion Model⏱️ Fixed duration & easing🌊 Physics (tension, friction, mass)
Layout Animation❌ Text content onlyβœ… Any CSS property + values
Interruption⚠️ Restarts or resetsβœ… Smooth interruption mid-flight

πŸ’‘ The Big Picture

react-countup is like a digital odometer πŸš—β€”it does one job perfectly and requires very little setup. Use it for dashboards, pricing tables, or any place where you just need a number to tick up reliably.

react-spring is like a physics lab πŸ§ͺβ€”it gives you the tools to build complex, interactive motion systems. Use it when the number animation is part of a larger interactive experience, like a game, a gesture-driven interface, or a highly polished marketing site.

Final Thought: If you only need to count up, react-countup saves time. If you need the count to feel alive and connected to other movements, react-spring is worth the extra code.

How to Choose: react-countup vs react-spring

  • react-countup:

    Choose react-countup if your primary goal is animating numeric values like statistics, prices, or scores. It handles formatting, easing, and decimal logic out of the box without extra configuration. This package is ideal when you need a quick, reliable counter without building custom animation logic. It reduces boilerplate significantly for standard number transitions.

  • react-spring:

    Choose react-spring if you need complex motion systems involving physics, gestures, or layout transitions. It allows you to animate numbers alongside other UI elements using a unified physics engine. This library is best for interactive interfaces where motion needs to feel natural and respond to user input. It offers greater flexibility but requires more setup for simple number counters.

README for react-countup

React CountUp

GitHub license Build Status Coverage Status Version Downloads Gzip size

A configurable React component wrapper around CountUp.js.

Click here to view on CodeSandbox.

Previous docs

react-countup

Table of Contents

Installation

yarn add react-countup

Usage

import CountUp from 'react-countup';

Simple example

<CountUp end={100} />

This will start a count up transition from 0 to 100 on render.

Render prop example

<CountUp
  start={-875.039}
  end={160527.012}
  duration={2.75}
  separator=" "
  decimals={4}
  decimal=","
  prefix="EUR "
  suffix=" left"
  onEnd={() => console.log('Ended! πŸ‘')}
  onStart={() => console.log('Started! πŸ’¨')}
>
  {({ countUpRef, start }) => (
    <div>
      <span ref={countUpRef} />
      <button onClick={start}>Start</button>
    </div>
  )}
</CountUp>

The transition won't start on initial render as it needs to be triggered manually here.

Tip: If you need to start the render prop component immediately, you can set delay={0}.

More examples

Manually start with render prop

<CountUp start={0} end={100}>
  {({ countUpRef, start }) => (
    <div>
      <span ref={countUpRef} />
      <button onClick={start}>Start</button>
    </div>
  )}
</CountUp>

Autostart with render prop

Render start value but start transition on first render:

<CountUp start={0} end={100} delay={0}>
  {({ countUpRef }) => (
    <div>
      <span ref={countUpRef} />
    </div>
  )}
</CountUp>

Note that delay={0} will automatically start the count up.

Delay start

<CountUp delay={2} end={100} />

Hook

Simple example

import { useCountUp } from 'react-countup';

const SimpleHook = () => {
  useCountUp({ ref: 'counter', end: 1234567 });
  return <span id="counter" />;
};

Complete example

import { useCountUp } from 'react-countup';

const CompleteHook = () => {
  const countUpRef = React.useRef(null);
  const { start, pauseResume, reset, update } = useCountUp({
    ref: countUpRef,
    start: 0,
    end: 1234567,
    delay: 1000,
    duration: 5,
    onReset: () => console.log('Resetted!'),
    onUpdate: () => console.log('Updated!'),
    onPauseResume: () => console.log('Paused or resumed!'),
    onStart: ({ pauseResume }) => console.log(pauseResume),
    onEnd: ({ pauseResume }) => console.log(pauseResume),
  });
  return (
    <div>
      <div ref={countUpRef} />
      <button onClick={start}>Start</button>
      <button onClick={reset}>Reset</button>
      <button onClick={pauseResume}>Pause/Resume</button>
      <button onClick={() => update(2000)}>Update to 2000</button>
    </div>
  );
};

API

Props

className: string

CSS class name of the span element.

Note: This won't be applied when using CountUp with render props.

decimal: string

Specifies decimal character.

Default: .

decimals: number

Amount of decimals to display.

Default: 0

delay: number

Delay in seconds before starting the transition.

Default: null

Note: delay={0} will automatically start the count up.

duration: number

Duration in seconds.

Default: 2

end: number

Target value.

prefix: string

Static text before the transitioning value.

redraw: boolean

Forces count up transition on every component update.

Default: false

preserveValue: boolean

Save previously ended number to start every new animation from it.

Default: false

separator: string

Specifies character of thousands separator.

start: number

Initial value.

Default: 0

plugin: CountUpPlugin

Define plugin for alternate animations

startOnMount: boolean

Use for start counter on mount for hook usage.

Default: true

suffix: string

Static text after the transitioning value.

useEasing: boolean

Enables easing. Set to false for a linear transition.

Default: true

useGrouping: boolean

Enables grouping with separator.

Default: true

useIndianSeparators: boolean

Enables grouping using indian separation, f.e. 1,00,000 vs 100,000

Default: false

easingFn: (t: number, b: number, c: number, d: number) => number

Easing function. Click here for more details.

Default: easeInExpo

formattingFn: (value: number) => string

Function to customize the formatting of the number.

To prevent component from unnecessary updates this function should be memoized with useCallback

enableScrollSpy: boolean

Enables start animation when target is in view.

scrollSpyDelay: number

Delay (ms) after target comes into view

scrollSpyOnce: boolean

Run scroll spy only once

onEnd: ({ pauseResume, reset, start, update }) => void

Callback function on transition end.

onStart: ({ pauseResume, reset, update }) => void

Callback function on transition start.

onPauseResume: ({ reset, start, update }) => void

Callback function on pause or resume.

onReset: ({ pauseResume, start, update }) => void

Callback function on reset.

onUpdate: ({ pauseResume, reset, start }) => void

Callback function on update.

Render props

countUpRef: () => void

Ref to hook the countUp instance to

pauseResume: () => void

Pauses or resumes the transition

reset: () => void

Resets to initial value

start: () => void

Starts or restarts the transition

update: (newEnd: number?) => void

Updates transition to the new end value (if given)

Protips

Trigger of transition

By default, the animation is triggered if any of the following props has changed:

  • duration
  • end
  • start

If redraw is set to true your component will start the transition on every component update.

Run if in focus

You need to check if your counter in viewport, react-visibility-sensor can be used for this purpose.

import React from 'react';
import CountUp from 'react-countup';
import VisibilitySensor from 'react-visibility-sensor';
import './styles.css';

export default function App() {
  return (
    <div className="App">
      <div className="content" />
      <VisibilitySensor partialVisibility offset={{ bottom: 200 }}>
        {({ isVisible }) => (
          <div style={{ height: 100 }}>
            {isVisible ? <CountUp end={1000} /> : null}
          </div>
        )}
      </VisibilitySensor>
    </div>
  );
}

Note: For latest react-countup releases there are new options enableScrollSpy and scrollSpyDelay which enable scroll spy, so that as user scrolls to the target element, it begins counting animation automatically once it has scrolled into view.

import './styles.css';
import CountUp, { useCountUp } from 'react-countup';

export default function App() {
  useCountUp({
    ref: 'counter',
    end: 1234567,
    enableScrollSpy: true,
    scrollSpyDelay: 1000,
  });

  return (
    <div className="App">
      <div className="content" />
      <CountUp end={100} enableScrollSpy />
      <br />
      <span id="counter" />
    </div>
  );
}

Set accessibility properties for occupation period

You can use callback properties to control accessibility:

import React from 'react';
import CountUp, { useCountUp } from 'react-countup';

export default function App() {
  useCountUp({ ref: 'counter', end: 10, duration: 2 });
  const [loading, setLoading] = React.useState(false);

  const onStart = () => {
    setLoading(true);
  };

  const onEnd = () => {
    setLoading(false);
  };

  const containerProps = {
    'aria-busy': loading,
  };

  return (
    <>
      <CountUp
        end={123457}
        duration="3"
        onStart={onStart}
        onEnd={onEnd}
        containerProps={containerProps}
      />
      <div id="counter" aria-busy={loading} />
    </>
  );
}

Plugin usage

import { CountUp } from 'countup.js';
import { Odometer } from 'odometer_countup';

export default function App() {
  useCountUp({
    ref: 'counter',
    end: 1234567,
    enableScrollSpy: true,
    scrollSpyDelay: 1000,
    plugin: Odometer,
  });

  return (
    <div className="App">
      <span id="counter" />
    </div>
  );
}

License

MIT