# react-countdown **Repository Path**: trusted-list/react-countdown ## Basic Information - **Project Name**: react-countdown - **Description**: A customizable countdown component for React. - **Primary Language**: Unknown - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-09-08 - **Last Updated**: 2026-09-15 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # React <Countdown /> [![npm][npm]][npm-url] [![CI: Build Status](https://img.shields.io/github/actions/workflow/status/ndresx/react-countdown/main.yml)](https://github.com/ndresx/react-countdown/actions/workflows/main.yml) [![Coverage Status](https://coveralls.io/repos/github/ndresx/react-countdown/badge.svg?branch=master)](https://coveralls.io/github/ndresx/react-countdown?branch=master) A customizable countdown component and Hook for React. - [Getting Started](#getting-started) - [Motivation](#motivation) - [Examples](#examples) - [Props](#props) - [Hook](#hook) - [Lifecycle Callbacks](#lifecycle-callbacks) - [API Reference](#api-reference) - [Helpers](#helpers) - [FAQ](#faq) - [Changelog](#changelog) - [Contributing](#contributing) - [License](#license) ## 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](https://github.com/ndresx/react-countdown/tree/v2.3.6). See the [Changelog](#changelog) for what changed and how to migrate. You can install the module via `npm`, `pnpm`, or `yarn`: ```sh npm install react-countdown --save ``` ```sh pnpm add react-countdown ``` ```sh 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) Hook from the package root: ```js 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: ```js 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. ```js import { createRoot } from 'react-dom/client'; import Countdown from 'react-countdown'; createRoot(document.getElementById('root')).render(); ``` [Live Demo](https://codesandbox.io/s/cool-fermat-uk0dq) ### 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`](#oncomplete) callback and/or custom [`renderer`](#renderer). ```js import { createRoot } from 'react-dom/client'; import Countdown from 'react-countdown'; // Random component const Completionist = () => You are good to go!; // Renderer callback with completed condition const renderer = ({ hours, minutes, seconds, completed }) => { return completed ? ( ) : ( {hours}:{minutes}:{seconds} ); }; createRoot(document.getElementById('root')).render( ); ``` [Live Demo](https://codesandbox.io/s/sad-zhukovsky-hs7hc) ### 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`](#intervaldelay) value needs to be lower than `1000`ms and a [`precision`](#precision) of `1` to `3` should be used. Last but not least, a simple [`renderer`](#renderer) callback needs to be set up. ```js import { createRoot } from 'react-dom/client'; import Countdown from 'react-countdown'; createRoot(document.getElementById('root')).render(
{props.total}
} /> ); ``` [Live Demo](https://codesandbox.io/s/elastic-euclid-6vnlw) ### Building a Stopwatch A stopwatch is simply a countdown with [`overtime`](#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'](#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()`](#start), [`pause()`](#pause), [`stop()`](#stop)) works unchanged. ```js import { createRoot } from 'react-dom/client'; import Countdown from 'react-countdown'; createRoot(document.getElementById('root')).render( (
{formatted.hours}:{formatted.minutes}:{formatted.seconds}
)} /> ); ``` > 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) Hook to render and control the countdown. ```js 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 {api.isCompleted() ? 'Completionist!' : `${hours}:${minutes}:${seconds}`} ; }; createRoot(document.getElementById('root')).render(); ``` > Note: `useRef` could be omitted here by setting [`freezeProps`](#freezeprops) to `true`. [Live Demo](https://codesandbox.io/s/kyLRX0yX) ## Props | Name | Type | Default | Description | | :---------------------------------- | :---------------------------------------: | :-----------: | :------------------------------------------------------------------------------------------------------------------------- | | [**date**](#date) | Date|string|number | required | [`Date`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date) or timestamp in the future | | [**daysInHours**](#daysinhours) | `boolean` | `false` | Days are calculated as hours | | [**zeroPadTime**](#zeropadtime) | `number` | `2` | Length of zero-padded output, e.g.: `00:01:02` | | [**zeroPadDays**](#zeropaddays) | `number` | `zeroPadTime` | Length of zero-padded days output, e.g.: `01` | | [**controlled**](#controlled) | `boolean` | `false` | Hands over the control to its parent(s) | | [**intervalDelay**](#intervaldelay) | `number` | `1000` | Interval delay in milliseconds | | [**precision**](#precision) | `number` | `0` | The precision on a millisecond basis | | [**autoStart**](#autostart) | `boolean` | `true` | Countdown auto-start option | | [**overtime**](#overtime) | `boolean` | `false` | Counts down to infinity | | [**renderer**](#renderer)\* | `function` | `undefined` | Custom renderer callback | | [**now**](#now) | `function` | `Date.now` | Alternative handler for the current date | | [**freezeProps**](#freezeprops) | `boolean` | `false` | Ignore all prop changes after mount | | [**resetKey**](#resetkey) | string|number | `undefined` | Change this value to restart the countdown (component + Hook) | | [**onMount**](#onmount) | `function` | `undefined` | Callback when component mounts | | [**onStart**](#onstart) | `function` | `undefined` | Callback when countdown starts | | [**onPause**](#onpause) | `function` | `undefined` | Callback when countdown pauses | | [**onStop**](#onstop) | `function` | `undefined` | Callback when countdown stops | | [**onTick**](#ontick) | `function` | `undefined` | Callback on every interval tick (`controlled` = `false`) | | [**onComplete**](#oncomplete) | `function` | `undefined` | Callback when countdown ends | \*Not used by the [`useCountdown`](#hook). ### `date` The `date` prop is the only required one and can be a [`Date`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/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`](#now). Valid values can be _(and more)_: - `'2020-02-01T01:02:03'` // [`Date` time string format](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/parse#Date_Time_String_Format) - `1580518923000` // Timestamp in milliseconds - `new Date(1580518923000)` // [`Date`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date) object ### `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`](#daysinhours) is `true` (since hours then absorb the days and can exceed `99`), otherwise the separate days field _(see [`zeroPadDays`](#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`](#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`](#date) will be treated as the countdown's actual time difference and not be compared to [`now`](#now) anymore. ### `intervalDelay` Defaults to `1000`ms. With higher [`precision`](#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`](#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., `10123`ms => `10000`ms), a precision of `3` leads to `10123`ms. ### `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`](#oncomplete) callback would still get triggered when the initial countdown phase completes. See [Building a Stopwatch](#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](#render-props) as the first argument. #### Render Props The render props object consists of the current time delta object, the countdown's [`api`](#api-reference), the component [`props`](#props), and last but not least, a [`formatted`](#formattimedelta) object. ```js { 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) 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`](#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](#why-does-my-countdown-reset-on-every-re-render-). ### `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 [``](#getting-started) component and the [`useCountdown`](#hook) Hook. > Prefer other options where they fit, for example, simply updating the [`date`](#date) prop. On the component, React's built-in [`key`](https://react.dev/learn/rendering-lists#keeping-list-items-in-order-with-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](#props) as its component counterpart, with the exception that the [`renderer`](#renderer) prop is not supported (it is omitted from the Hook's type) due to the flexibility the Hook already provides. ```js const { total, api } = useCountdown(props); ``` Using the Hook gives direct access to the countdown's [API](#api-reference), current [time delta object](#calctimedelta), formatted values and more as part of the [render props](#render-props) object. ## Lifecycle Callbacks The countdown invokes a set of optional lifecycle callbacks that you pass as props (also listed in the [props](#props) table). Each one receives a [time delta object](#calctimedelta) as its first argument; [`onComplete`](#oncomplete) additionally receives a second argument. ### `onMount` `onMount` is a callback and triggered when the countdown mounts. It receives a [time delta object](#calctimedelta) 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](#calctimedelta) as the first argument. ### `onPause` `onPause` is a callback and triggered every time the countdown is paused. It receives a [time delta object](#calctimedelta) as the first argument. ### `onStop` `onStop` is a callback and triggered every time the countdown is stopped. It receives a [time delta object](#calctimedelta) as the first argument. ### `onTick` `onTick` is a callback and triggered every time a new period is started, based on what the [`intervalDelay`](#intervaldelay)'s value is. It only gets triggered when the countdown's [`controlled`](#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](#calctimedelta) whose `completed` is `true`, and [`onComplete`](#oncomplete) is then fired in addition. It receives a [time delta object](#calctimedelta) as the first argument. ### `onComplete` `onComplete` is a callback and triggered whenever the countdown ends. In contrast to [`onTick`](#ontick), the [`onComplete`](#oncomplete) callback also gets triggered in case [`controlled`](#controlled) is set to `true`. It receives a [time delta object](#calctimedelta) 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`](#getting-started) type), inside a custom [`renderer`](#renderer) via the [render props](#render-props), and as part of the object returned by the [`useCountdown`](#hook) Hook. Here's an [example](https://github.com/ndresx/react-countdown/blob/master/examples/src/CountdownApi.tsx) of how to use it. ### `start()` Starts the countdown in case it is paused/stopped or needed when [`autoStart`](#autostart) is set to `false`. ### `pause()` Pauses the running countdown. This only works as expected if the [`controlled`](#controlled) prop is set to `false` because [`calcTimeDelta`](#calctimedelta) calculates an offset time internally. ### `stop()` Stops the countdown. This only works as expected if the [`controlled`](#controlled) prop is set to `false` because [`calcTimeDelta`](#calctimedelta) calculates an offset time internally. ### `refresh()` Forces an immediate recompute of the [time delta](#calctimedelta) 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: ```jsx 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`](#ontick); and if the recomputed time has crossed zero, it fires [`onComplete`](#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`](#overtime) is `true`. Nevertheless, when [`overtime`](#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()`](#iscompleted), the status never becomes `COMPLETED` while [`overtime`](#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`](#renderer). ```js 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`. ```js const renderer = ({ hours, minutes, seconds }) => ( {zeroPad(hours)}:{zeroPad(minutes)}:{zeroPad(seconds)} ); ``` ### `calcTimeDelta(date, [options])` `calcTimeDelta` calculates the time difference between a given end [`date`](#date) and the current date (`now`). It returns, similar to the [`renderer`](#renderer) callback, a custom object (also referred to as **countdown time delta object**) with the following time-related data: ```js { 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`](#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`](#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`](#now). - **`precision = 0`** The [`precision`](#precision) on a millisecond basis. - **`controlled = false`** Defines whether the calculated value is provided in a [`controlled`](#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`](#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](#calctimedelta). It returns the formatted portion of it, equivalent to: ```js { 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`](#calctimedelta). **`options`** The `options` object consists of the following three component props and is used to customize the time delta object's formatting: - [`daysInHours`](#daysinhours) - [`zeroPadTime`](#zeropadtime) - [`zeroPadDays`](#zeropaddays) ## FAQ ### Why does my countdown reset on every re-render? A common reason for this is that the [`date`](#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) Hook, the date could be persisted via React's [`useRef`](https://react.dev/reference/react/useRef) before it gets passed in. Alternatively, the countdown is providing a [`freezeProps`](#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`](#renderer)? The [`renderer`](#renderer) callback gets called with a [time delta object](#calctimedelta) 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: ```tsx const [mounted, setMounted] = useState(false); useEffect(() => setMounted(true), []); if (!mounted) return null; // render nothing, or your own placeholder, until client-side return ; ``` Alternatively, wrap the output in [`suppressHydrationWarning`](https://react.dev/reference/react-dom/components/common#suppressing-unavoidable-hydration-mismatch-errors) to silence the warning, or set [`autoStart`](#autostart) to `false` and start the countdown through the [API](#api-reference) once it is available on the client. Here are some related [issues](https://github.com/ndresx/react-countdown/issues/152) that might help. ## Changelog See the [CHANGELOG](https://github.com/ndresx/react-countdown/blob/master/CHANGELOG.md) for the full release history. Upgrading from 2.x? Start with the [Migrating from 2.x](https://github.com/ndresx/react-countdown/blob/master/CHANGELOG.md#migrating-from-2x) guide. ## Contributing Contributions of any kind are very welcome. Read more in our [contributing guide](https://github.com/ndresx/react-countdown/blob/master/CONTRIBUTING.md) about how to report bugs, create pull requests, and other development-related topics. ## License MIT [npm]: https://img.shields.io/npm/v/react-countdown.svg [npm-url]: https://npmjs.com/package/react-countdown