Skip to content

ndresx/react-countdown

Repository files navigation

React <Countdown /> npm CI: Build Status Coverage Status

A customizable countdown component and Hook for React.

Getting Started

v3 is in beta. Install it with npm install react-countdown@next. The current stable release is documented at the v2.3.6 tag. See the Changelog for what changed and how to migrate.

You can install the module via npm, pnpm, or yarn:

npm install react-countdown --save
pnpm add react-countdown
yarn add react-countdown

Imports

The package ships as both ES modules and CommonJS, is fully typed, and is side-effect free, so bundlers can tree-shake it. Import the component and the useCountdown Hook from the package root:

import Countdown, { useCountdown } from 'react-countdown';

If you only need one of them, dedicated entry points let your bundler include just that part (plus the shared core) and leave the rest out:

import Countdown from 'react-countdown/component';
import { useCountdown } from 'react-countdown/hook';

Motivation

This countdown started as part of a small web app. Extracting it into its own package was a way to explore the broader aspects of building with React, testing with Jest, and publishing an open source project.

Examples

Here are some examples which you can try directly online. You can also clone this repo and explore some more examples in there by running pnpm start within the examples folder.

Basic Usage

A very simple and minimal example of how to set up a countdown that counts down from 10 seconds.

import { createRoot } from 'react-dom/client';
import Countdown from 'react-countdown';

createRoot(document.getElementById('root')).render(<Countdown date={Date.now() + 10000} />);

Live Demo

Custom & Conditional Rendering

In case you want to change the output of the component, or want to signal that the countdown's work is done, you can do this by defining an onComplete callback and/or custom renderer.

import { createRoot } from 'react-dom/client';
import Countdown from 'react-countdown';

// Random component
const Completionist = () => <span>You are good to go!</span>;

// Renderer callback with completed condition
const renderer = ({ hours, minutes, seconds, completed }) => {
  return completed ? (
    <Completionist />
  ) : (
    <span>
      {hours}:{minutes}:{seconds}
    </span>
  );
};

createRoot(document.getElementById('root')).render(
  <Countdown date={Date.now() + 5000} renderer={renderer} />
);

Live Demo

Countdown in Milliseconds

Here is an example with a countdown of 10 seconds that displays the total time difference in milliseconds. In order to display the milliseconds appropriately, the intervalDelay value needs to be lower than 1000ms and a precision of 1 to 3 should be used. Last but not least, a simple renderer callback needs to be set up.

import { createRoot } from 'react-dom/client';
import Countdown from 'react-countdown';

createRoot(document.getElementById('root')).render(
  <Countdown
    date={Date.now() + 10000}
    intervalDelay={0}
    precision={3}
    renderer={(props) => <div>{props.total}</div>}
  />
);

Live Demo

Building a Stopwatch

A stopwatch is simply a countdown with overtime enabled that starts at the current time: the time delta immediately crosses 0 and keeps running, so the elapsed time counts up. Because the render props' formatted values are based on the absolute time delta, rendering them (instead of relying on the default renderer, which prefixes a - once total turns negative) reads as a regular count-up timer. The countdown's api (start(), pause(), stop()) works unchanged.

import { createRoot } from 'react-dom/client';
import Countdown from 'react-countdown';

createRoot(document.getElementById('root')).render(
  <Countdown
    date={Date.now()}
    overtime
    daysInHours
    autoStart={false}
    renderer={({ formatted, api }) => (
      <div>
        <span>
          {formatted.hours}:{formatted.minutes}:{formatted.seconds}
        </span>
        <button onClick={api.start}>{api.isPaused() ? 'Resume' : 'Start'}</button>
        <button onClick={api.pause}>Pause</button>
      </div>
    )}
  />
);

Note: because the timer starts at 0, the time delta's total is negative and completed is true from the first tick onwards. The formatted values stay positive, which is why the renderer above doesn't need to handle the sign.

Using the useCountdown Hook

If you prefer React Hooks over normal components, you can also use the built-in useCountdown Hook to render and control the countdown.

import { useRef } from 'react';
import { createRoot } from 'react-dom/client';
import { useCountdown } from 'react-countdown';

// Function component
const MyComponent = () => {
  const props = useRef({ date: Date.now() + 5000 });
  const { hours, minutes, seconds, api } = useCountdown(props.current);
  return <span>{api.isCompleted() ? 'Completionist!' : `${hours}:${minutes}:${seconds}`} </span>;
};

createRoot(document.getElementById('root')).render(<MyComponent />);

Note: useRef could be omitted here by setting freezeProps to true.

Live Demo

Props

Name Type Default Description
date Date|string|number required Date or timestamp in the future
daysInHours boolean false Days are calculated as hours
zeroPadTime number 2 Length of zero-padded output, e.g.: 00:01:02
zeroPadDays number zeroPadTime Length of zero-padded days output, e.g.: 01
controlled boolean false Hands over the control to its parent(s)
intervalDelay number 1000 Interval delay in milliseconds
precision number 0 The precision on a millisecond basis
autoStart boolean true Countdown auto-start option
overtime boolean false Counts down to infinity
renderer* function undefined Custom renderer callback
now function Date.now Alternative handler for the current date
freezeProps boolean false Ignore all prop changes after mount
resetKey string|number undefined Change this value to restart the countdown (component + Hook)
onMount function undefined Callback when component mounts
onStart function undefined Callback when countdown starts
onPause function undefined Callback when countdown pauses
onStop function undefined Callback when countdown stops
onTick function undefined Callback on every interval tick (controlled = false)
onComplete function undefined Callback when countdown ends

*Not used by the useCountdown.

date

The date prop is the only required one and can be a Date object, string, or timestamp in the future. By default, this date is compared with the current date, or a custom handler defined via now.

Valid values can be (and more):

daysInHours

Defines whether the time of day should be calculated as hours rather than separated days.

zeroPadTime

This option defaults to 2 in order to display the common format 00:00:00 instead of 0:0:0. Minutes and seconds are always capped at a padding width of 2. A value higher than 2 only widens the leading field: the hours when daysInHours is true (since hours then absorb the days and can exceed 99), otherwise the separate days field (see zeroPadDays). If the value is lower, the output won't be zero-padded like the example before is showing.

zeroPadDays

Defaults to zeroPadTime. It works the same way as zeroPadTime does, just for days.

controlled

Can be useful if the countdown's interval and/or date control should be handed over to the parent. In case controlled is true, the provided date will be treated as the countdown's actual time difference and not be compared to now anymore.

intervalDelay

Defaults to 1000ms. With higher precision, the intervalDelay could be set to something lower - down to 0, which would, for example, allow showing the milliseconds in a more fancy way (only possible by using a custom renderer).

precision

In certain cases, you might want to base off the calculations on a millisecond basis. The precision prop, which defaults to 0, can be used to refine this calculation. While the default value simply strips the milliseconds part (e.g., 10123ms => 10000ms), a precision of 3 leads to 10123ms.

autoStart

Defines whether the countdown should start automatically or not. Defaults to true.

overtime

Defines whether the countdown can go into overtime by extending its lifetime past the targeted endpoint. Defaults to false.

When set to true, the countdown timer won't stop when hitting 0, but instead becomes negative and continues to run unless paused/stopped. The onComplete callback would still get triggered when the initial countdown phase completes.

See Building a Stopwatch for a common use of this option.

renderer

The component's raw render output is kept very simple.

For more advanced countdown displays, a custom renderer callback can be defined to return a new React element. It receives the following render props as the first argument.

Render Props

The render props object consists of the current time delta object, the countdown's api, the component props, and last but not least, a formatted object.

{
  total: 0,
  days: 0,
  hours: 0,
  minutes: 0,
  seconds: 0,
  milliseconds: 0,
  completed: true,
  api: { ... },
  props: { ... },
  formatted: { ... }
}

now

If the current date and time (determined via a reference to Date.now) is not the right thing to compare with for you, a reference to a custom function that returns a similar dynamic value could be provided as an alternative.

freezeProps

By default, the countdown component and useCountdown Hook track their props. This means that whenever an input prop changes, the countdown will update accordingly and re-render itself based on the new conditions.

However, this behavior is not always desired and sometimes requires more lines of code than needed, which is why it can be turned off as well. With freezeProps set to true, the countdown ignores all prop changes after mounting and keeps running from its initial props, so a date that gets recomputed on every render no longer restarts it, without having to persist it yourself. Note that this applies to every other prop, so other changes (e.g. daysInHours or a callback) also won't take effect while freezeProps is true.

freezeProps itself is read live, so it is not a one-way latch: toggling it back to false re-enables prop tracking, and the next prop change is picked up again.

Read more about this "issue" here.

resetKey

A library-owned token used to restart the countdown. Passing in a new string or number value recreates it from scratch with its current props. It behaves identically on both the <Countdown /> component and the useCountdown Hook.

Prefer other options where they fit, for example, simply updating the date prop. On the component, React's built-in key prop also forces a full remount; resetKey is the equivalent that additionally works inside the Hook.

Hook

Requires React 18 or higher

The library also ships with a built-in Hook called useCountdown. It supports the same props as its component counterpart, with the exception that the renderer prop is not supported (it is omitted from the Hook's type) due to the flexibility the Hook already provides.

const { total, api } = useCountdown(props);

Using the Hook gives direct access to the countdown's API, current time delta object, formatted values and more as part of the render props object.

Lifecycle Callbacks

The countdown invokes a set of optional lifecycle callbacks that you pass as props (also listed in the props table). Each one receives a time delta object as its first argument; onComplete additionally receives a second argument.

onMount

onMount is a callback and triggered when the countdown mounts. It receives a time delta object as the first argument.

onStart

onStart is a callback and triggered whenever the countdown is started (including first-run). It receives a time delta object as the first argument.

onPause

onPause is a callback and triggered every time the countdown is paused. It receives a time delta object as the first argument.

onStop

onStop is a callback and triggered every time the countdown is stopped. It receives a time delta object as the first argument.

onTick

onTick is a callback and triggered every time a new period is started, based on what the intervalDelay's value is. It only gets triggered when the countdown's controlled prop is set to false, meaning that the countdown has full control over its interval. This includes the tick that completes the countdown: onTick fires for it with a time delta object whose completed is true, and onComplete is then fired in addition. It receives a time delta object as the first argument.

onComplete

onComplete is a callback and triggered whenever the countdown ends. In contrast to onTick, the onComplete callback also gets triggered in case controlled is set to true. It receives a time delta object as the first argument and a boolean as a second argument, indicating whether the countdown transitioned into the completed state (false) or completed on start (true).

API Reference

The countdown exposes a small control API as api. It is the same object reached three ways: on the component ref as ref.current.api (e.g. ref.current.api.start(); type the ref with the exported CountdownHandle type), inside a custom renderer via the render props, and as part of the object returned by the useCountdown Hook. Here's an example of how to use it.

start()

Starts the countdown in case it is paused/stopped or needed when autoStart is set to false.

pause()

Pauses the running countdown. This only works as expected if the controlled prop is set to false because calcTimeDelta calculates an offset time internally.

stop()

Stops the countdown. This only works as expected if the controlled prop is set to false because calcTimeDelta calculates an offset time internally.

refresh()

Forces an immediate recompute of the time delta and a re-render against the current clock, without changing the countdown's status or offsets. This is useful when the internal interval has been throttled (for example, while the tab was in the background) and you want the displayed value to update right away instead of waiting for the next tick, e.g. from a visibilitychange listener:

React.useEffect(() => {
  const onVisible = () => {
    if (!document.hidden) ref.current?.api.refresh();
  };

  document.addEventListener('visibilitychange', onVisible);
  return () => document.removeEventListener('visibilitychange', onVisible);
}, []);

Like a regular tick, it triggers onTick; and if the recomputed time has crossed zero, it fires onComplete just as the final interval tick would.

isStarted()

Returns a boolean for whether the countdown is currently started (running) or not.

isPaused()

Returns a boolean for whether the countdown has been paused or not.

isStopped()

Returns a boolean for whether the countdown has been stopped or not.

isCompleted()

Returns a boolean for whether the countdown has been completed or not.

Please note that this will always return false if overtime is true. Nevertheless, when overtime is enabled, the countdown's completed state can still be read from the time delta object's completed value.

getStatus()

Returns the countdown's current status as a CountdownStatus enum value: STARTED, PAUSED, STOPPED, or COMPLETED. Use this when you need the exact state rather than one of the boolean predicates above. The CountdownStatus enum is exported from the package (and from the react-countdown/component and react-countdown/hook entry points) for typing and comparison.

Like isCompleted(), the status never becomes COMPLETED while overtime is true, since the countdown keeps running past zero.

Helpers

This module also exports three simple helper functions, which can be utilized to build your own countdown custom renderer.

import Countdown, { zeroPad, calcTimeDelta, formatTimeDelta } from 'react-countdown';

zeroPad(value, [length = 2])

The zeroPad function transforms and returns a given value with padded zeros depending on the length. The value can be a string or number, while the length parameter can be a number, defaulting to 2. Returns the zero-padded string, e.g., zeroPad(5) => 05.

const renderer = ({ hours, minutes, seconds }) => (
  <span>
    {zeroPad(hours)}:{zeroPad(minutes)}:{zeroPad(seconds)}
  </span>
);

calcTimeDelta(date, [options])

calcTimeDelta calculates the time difference between a given end date and the current date (now). It returns, similar to the renderer callback, a custom object (also referred to as countdown time delta object) with the following time-related data:

{
  total: number;
  days: number;
  hours: number;
  minutes: number;
  seconds: number;
  milliseconds: number;
  completed: boolean;
}

The total value is the overall time difference in milliseconds. It is signed: with overtime enabled it can become negative once the target is passed, while the other time-related values contain the (always non-negative) relative portion of the current time difference. The completed value indicates whether the countdown reached its initial end or not.

The calcTimeDelta function accepts two arguments in total; only the first one is required.

date Date or timestamp representation of the end date. See date prop for more details.

options The second argument consists of the following optional keys.

  • now = Date.now Alternative function for returning the current date, also see now.

  • precision = 0 The precision on a millisecond basis.

  • controlled = false Defines whether the calculated value is provided in a controlled environment as the time difference or not.

  • offsetTime = 0 Defines the offset time that gets added to the start time; only considered if controlled is false.

  • overtime = false Defines whether the time delta can go into overtime and become negative or not. When set to true, the total could become negative, at which point completed will still be set to true.

formatTimeDelta(delta, [options])

formatTimeDelta formats a given countdown time delta object. It returns the formatted portion of it, equivalent to:

{
  days: '00',
  hours: '00',
  minutes: '00',
  seconds: '00',
}

This function accepts two arguments in total; only the first one is required.

timeDelta Time delta object, e.g., returned by calcTimeDelta.

options The options object consists of the following three component props and is used to customize the time delta object's formatting:

FAQ

Why does my countdown reset on every re-render?

A common reason for this is that the date prop gets passed directly into the component/Hook without persisting it in any way.

In order to avoid this from happening, it should be stored in a place that persists throughout lifecycle changes, for example, in the component's local state.

When using function components with the useCountdown Hook, the date could be persisted via React's useRef before it gets passed in.

Alternatively, the countdown is providing a freezeProps prop to turn off this behavior so that aforementioned precautions don't necessarily have to be taken manually.

Why aren't my values formatted when using the custom renderer?

The renderer callback gets called with a time delta object that also consists of a formatted object which holds these formatted values.

Why do I get this error "Warning: Text content did not match..."?

This is a server-side rendering hydration mismatch. The countdown's value is based on the current time, which advances between the server render and the client's hydration, so the two produce different output.

The countdown itself is SSR-safe (it does not touch window while rendering), so this is purely a rendering concern. The most reliable fix is to render the time-dependent output only after the client has mounted, so the server and the first client render match:

const [mounted, setMounted] = useState(false);
useEffect(() => setMounted(true), []);
if (!mounted) return null; // render nothing, or your own placeholder, until client-side
return <Countdown {...props} />;

Alternatively, wrap the output in suppressHydrationWarning to silence the warning, or set autoStart to false and start the countdown through the API once it is available on the client. Here are some related issues that might help.

Changelog

See the CHANGELOG for the full release history. Upgrading from 2.x? Start with the Migrating from 2.x guide.

Contributing

Contributions of any kind are very welcome. Read more in our contributing guide about how to report bugs, create pull requests, and other development-related topics.

License

MIT

About

A customizable countdown component for React.

Topics

Resources

License

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors