Skip to content
WingoUI
ComponentsTemplatesDocsPricingAI agentsBlogChangelog
  1. Home
  2. Blog
  3. Component guides
  4. React Countdown Timer: Blocks, Inline, Sleeping Tabs
Auf Deutsch lesen
  1. Blog
  2. Component guides
  3. React Countdown Timer: Blocks, Inline, Sleeping Tabs
Component guides
Component guides

React Countdown Timer: Blocks, Inline, Sleeping Tabs

SR
Serban Rusu · Founder of Wingo UI
Oct 19, 2026 · 9 min read

On this page

0%
  1. How should a React countdown timer compute the time left?
  2. Which countdown style fits: blocks, inline or words?
  3. Why does a countdown drift in a background tab?
  4. How do I handle time zones in a launch countdown?
  5. How do I render a countdown with server rendering?
  6. What happens when the countdown reaches zero?
  7. Is there a React countdown hook for custom layouts?
  8. How does the countdown work for screen readers and on phones?
  9. How do the React countdown libraries compare?
  10. Where does the countdown fit on a launch page?
  11. How do I add it to my project?
  12. FAQ
    1. How do I make a countdown timer in React?
    2. Why does my countdown timer slow down in a background tab?
    3. How do I show the same countdown in every time zone?
    4. Is there a React countdown hook?
    5. Is the Wingo UI Countdown free?

TL;DR

A React countdown timer should compute the time left from a fixed target timestamp on every tick instead of subtracting one second per tick, because browsers throttle timers in background tabs. Store the target as a UTC ISO string, render a placeholder until hydration, and let the server, not the countdown, decide when something actually opens. The Wingo UI Countdown handles this and renders as digit blocks, inline in a sentence or in words.

Published Oct 19, 2026

You need a React countdown timer for a launch, a sale that ends at midnight, an auction or a trial. The first version takes ten minutes: a number in state, setInterval, minus one every second. Then it ships: a tab left in the background shows the wrong time, Berlin and New York see the launch at different moments, and the server-rendered page logs a mismatch warning. This guide covers the three ways a countdown usually looks, the pitfalls behind those bugs, and the props that matter in the Wingo UI Countdown.

How should a React countdown timer compute the time left?

From the target, every time. Keep one fixed moment (to) and compute to - Date.now() on each tick. The tick only decides when to repaint; it never carries the time itself. A version that stores a remaining count and decrements it is the one that drifts.

A minimal hook that gets this right looks like this:

tsx
"use client"
import { useEffect, useState } from "react"
// ms left until `to`, or null before the browser clock is known
export function useTimeLeft(to: number) {
const [now, setNow] = useState<number | null>(null)
useEffect(() => {
const tick = () => setNow(Date.now())
tick()
const id = window.setInterval(tick, 1000)
return () => window.clearInterval(id)
}, [])
return now === null ? null : Math.max(0, to - now)
}

It still has two rough edges. The interval runs on its own one second grid, so the display can sit up to a second away from the real value and reach zero late. And it starts null, so you have to decide what to render on the server. A full React countdown timer component takes care of both, and of the parts below.

Which countdown style fits: blocks, inline or words?

Pick by where the countdown sits. The Countdown has three variants:

VariantRendersUse it for
blocks (default)A tile per unit with its label: 2 DAYS 04 HOURS 12 MINUTES 09 SECONDSA launch hero, a sale banner, a maintenance page
inline2d 04:12:09 in the flow of a sentence"Auction ends in 3:12:45" on a listing card
textWords: "in 2 days and 4 hours"Trial notices, emails, anything calm

Blocks roll each changed digit into place like an odometer, using the free Animated Counter underneath (animation="fade" crossfades instead, "none" swaps plain text). Leading zero units drop out as time passes, so a launch two days away shows days, while the last hour shows only minutes and seconds. It never shows "0 days".

Pick in 10 s, in 2 days, in 45 days or past under the card to watch it tick, finish or render done; switch the variant and the tile style in the playground
$ npx wingo-ui@latest add countdown
ProCountdown docs

In a launch hero, the countdown itself is one element:

tsx
import { Countdown } from "@/components/ui/countdown"
export function LaunchHero() {
return (
<section className="flex flex-col items-center gap-6 py-16 text-center">
<h1 className="text-4xl font-semibold tracking-tight">Acme Mobile lands soon</h1>
<Countdown to="2026-11-17T17:00:00Z" size="lg" tone="brand" completedContent="Acme Mobile is live" />
</section>
)
}

The inline and text variants render a <span>, so they can sit inside a paragraph. The blocks variant renders a <div>, and a <div> inside a <p> is invalid HTML that Next.js reports as a hydration error, so keep blocks outside paragraphs.

tsx
<p className="text-sm text-muted-foreground">
Auction ends in <Countdown to={auction.endsAt} variant="inline" size="sm" />
</p>
<p className="text-sm">
Your trial ends <Countdown to={trial.endsAt} variant="text" showSeconds={false} />.
</p>

precision sets how many units the text variant names (2 by default, "in 3 days and 4 hours"). showSeconds={false} hides seconds and ticks once a minute, which is calmer and cheaper on a long list of cards.

The text variant in a sentence: Launch starts in 2 days and 4 hours. Pick in 10 s to see the words change to Time's up
$ npx wingo-ui@latest add countdown
ProCountdown docs

Why does a countdown drift in a background tab?

Because browsers throttle timers in tabs nobody is looking at. According to Chrome's timer throttling notes (opens in a new tab), timers in a hidden page are checked once per second, and from Chrome 88, once per minute when the page has been hidden for more than 5 minutes, has been silent for 30 seconds, its timers are chained 5 or more deep and WebRTC is not in use. MDN (opens in a new tab) lists a 1 second minimum in inactive Firefox desktop tabs and 15 minutes on Firefox for Android. A laptop that sleeps runs no timers at all.

A countdown that subtracts one per tick loses every skipped tick. One that recomputes from the target is only late to repaint. The Countdown goes one step further: it schedules each repaint for the exact moment the value crosses a unit boundary of the target, so the last tick lands on zero, and it recomputes as soon as the tab becomes visible again, so a tab that slept jumps straight to the right value.

There is a second timer trap for long countdowns. setTimeout stores its delay as a signed 32-bit integer, so the maximum is 2,147,483,647 ms, about 24.8 days. MDN notes that larger values overflow, and an overflowed delay can run at once. A setTimeout(openSale, saleAt - Date.now()) for a sale 45 days away can fire immediately. The Countdown never waits longer than one tick, and it caps any single wait at that limit, so a 45 day launch counts down normally.

How do I handle time zones in a launch countdown?

Store one absolute moment and format it per visitor. A launch at noon in New York on November 17, 2026 is 2026-11-17T17:00:00Z (Eastern time is UTC-5 then). Every browser counts down to that same instant, so the finish happens at the same moment everywhere.

The two formats to avoid, from MDN's Date.parse page (opens in a new tab):

  • A date-time without an offset ("2026-11-17T12:00:00") is read as local time on each visitor's device. Berlin hits zero six hours before New York.
  • A date-only string ("2026-11-17") is read as midnight UTC, which is 7 pm on November 16 in New York.

When you print the launch time next to the countdown, format it in the visitor's own zone and name the zone, so "Nov 17, 2026, 12:00 PM EST" means the same thing to everyone:

ts
const launchLabel = new Intl.DateTimeFormat("en-US", {
month: "short",
day: "numeric",
year: "numeric",
hour: "numeric",
minute: "2-digit",
timeZoneName: "short",
}).format(new Date("2026-11-17T17:00:00Z"))

Spell out the fields here. dateStyle and timeStyle cannot be combined with timeZoneName, and that combination throws a TypeError.

How do I render a countdown with server rendering?

Render a placeholder on the server and the digits after hydration. The server does not know the visitor's clock, and a statically prerendered page would freeze whatever time it computed at build time. The Next.js hydration error page (opens in a new tab) lists time-dependent APIs like the Date() constructor in render logic as a common cause of mismatches.

The Countdown reads the clock through useSyncExternalStore with a server snapshot of null. Until the clock is known, each unit renders -- over an invisible 00 at its final width, so nothing jumps when the real digits arrive and no mismatch is logged. The root's data-state is idle then, running while it counts and done at zero.

now pins the clock for screenshots and tests; a pinned countdown does not tick.

What happens when the countdown reaches zero?

onComplete fires once on the transition to zero, on time even when seconds are hidden. completedContent replaces the digits and springs in. A to that is already past when the component mounts renders the done state without calling onComplete, so a visitor who opens the page after the sale ended does not get a "sale just ended" toast.

tsx
"use client"
import { useRouter } from "next/navigation"
import { Countdown } from "@/components/ui/countdown"
export function SaleTimer({ endsAt }: { endsAt: string }) {
const router = useRouter()
return (
<Countdown
to={endsAt}
size="sm"
tone="danger"
completedContent="The offer has ended"
// ask the server for the post-sale page
onComplete={() => router.refresh()}
/>
)
}

The countdown is a display. It trusts the device clock, and a device clock can be minutes off or changed by hand. If the moment matters (a checkout price, an auction close, early access), the server checks the time when the request arrives. The countdown only tells people when to come back.

Is there a React countdown hook for custom layouts?

Yes. useCountdown is the clock behind the component, exported from the same file. It returns { days, hours, minutes, seconds, totalMs, done }, recomputed from the absolute time on every tick, with totalMs set to null until the clock is known. Its options are now, interval (default 1000), onComplete and units. With units: ["seconds"] the seconds carry the whole remainder, which suits a resend timer:

tsx
"use client"
import { useState } from "react"
import { Button } from "@/components/ui/button"
import { useCountdown } from "@/components/ui/countdown"
export function ResendCode({ onResend }: { onResend: () => void }) {
const [to, setTo] = useState(() => Date.now() + 60_000)
const { seconds, done, totalMs } = useCountdown(to, { units: ["seconds"] })
const left = totalMs === null ? 60 : seconds
return (
<Button
variant="soft"
disabled={!done}
onClick={() => {
onResend()
setTo(Date.now() + 60_000)
}}
>
{done ? "Resend code" : `Resend in ${left} s`}
</Button>
)
}

The Countdown docs pair this react countdown hook with the free Progress ring for a 60 second OTP resend, the same pattern as the React OTP input guide.

How does the countdown work for screen readers and on phones?

The root has role="timer" with aria-live="off", because a live region that changes every second would interrupt a screen reader user every second. A hidden sentence ("2 days, 4 hours and 12 minutes left") updates once a minute for anyone who navigates to it, and the finish is announced once, politely. Reduced motion turns the rolling digits into plain swaps.

On a phone, the lg blocks shrink their numerals and tiles below 400px of their own width, so four units still fit a 360px screen. The root carries data-urgent in the last minute for a color change near the end. Every string lives in labels (days, hours, minutes, seconds, names, short, sentence, done, and, in), with plural functions per unit, so a translation is a prop instead of a fork.

How do the React countdown libraries compare?

react-countdown is the headless choice, react-countdown-circle-timer draws a ring, and the Wingo UI Countdown gives you finished layouts as source. As of October 2026, from each project's public README:

OptionModelCountsNotes
react-countdown (opens in a new tab)MIT package, component plus useCountdown hookTo a daterenderer prop for your own markup, intervalDelay, overtime past zero
react-countdown-circle-timer (opens in a new tab)MIT package, component plus useCountdown hookA duration in secondsAn SVG ring with color stops; the hook returns the path values
A custom hookYour codeWhatever you writeFine for one simple timer, as long as it recomputes from the target
Wingo UI CountdownPro source component in your repoTo toBlocks, inline and text variants, placeholders for SSR, aligned ticks, labels

For a ring around a 30 second quiz timer, react-countdown-circle-timer is the better fit. For headless logic with all your own markup, react-countdown is a solid choice.

Where does the countdown fit on a launch page?

At the top of a waitlist. The Waitlist Page template puts a Countdown under the headline, counting to launchAt, with a "live" note as its done content, and turns into a plain coming soon page when launchAt is null. The Next.js waitlist page tutorial wires it to a real signup table, a place in line and referral links, so the launch countdown React page you ship before day one collects signups instead of only ticking. For a single number that rolls instead of a full timer, see the animated number counter guide, and for relative dates like "3 hours ago" there is the Relative Time component.

How do I add it to my project?

Run npx wingo-ui@latest add countdown. The CLI copies components/ui/countdown.tsx into your project with the Animated Counter and the helpers it imports, and installs motion. The Countdown and the Waitlist Page are part of Wingo UI Pro. More single-component deep dives are in the component guides category.

Components in this post

  • Countdown

    A React countdown timer to a launch, sale or deadline, shown as rolling digit blocks, inline in a sentence or in words, with done content.

    Pro
  • Waitlist Page

    React waitlist page template: a launch countdown, an email signup with social proof, your place in line with a referral link and FAQ.

    Pro

FAQ

How do I make a countdown timer in React?

Keep the target moment (a timestamp or an ISO string with an offset) and recompute target minus Date.now() on every tick inside a useEffect, then split the result into days, hours, minutes and seconds. Never decrement a counter, because a throttled or sleeping tab skips ticks and the counter falls behind.

Why does my countdown timer slow down in a background tab?

Browsers throttle timers in hidden tabs: Chrome checks them once per second, and once per minute after a page has been hidden for more than 5 minutes with a chain of timers. A countdown that subtracts one per tick loses those skipped ticks, while one that recomputes from the target shows the right value on the next tick.

How do I show the same countdown in every time zone?

Store the launch as one absolute moment, such as 2026-11-17T17:00:00Z, and let each browser count down to it. A date-time string without an offset is read as the visitor's local time and a date-only string as UTC midnight, so either one gives people in different zones different finish times.

Is there a React countdown hook?

Yes. The react-countdown package exports useCountdown, and the Wingo UI Countdown exports a useCountdown hook that returns days, hours, minutes, seconds, totalMs and done for your own layout. A short custom hook works too if it recomputes from the target.

Is the Wingo UI Countdown free?

No. The Countdown and the Waitlist Page template are Pro. The Animated Counter that rolls its digits and the Progress ring used in the resend example are free.

  • Countdown
  • Timers
  • React
  • Next.js
  • Accessibility

Share

SR

About the author

Serban Rusu

Founder of Wingo UI

Serban Rusu founded Wingo UI. He builds its component library, CLI and MCP server, and writes about React interfaces for phones and AI coding agents.

More from Serban
Keep reading

Related posts

All posts
Build it: screens and apps

Nextjs Waitlist Page: Countdown, Referrals, Real Counts

Build a Nextjs waitlist page with a launch countdown, email signup, place in line and referral link on Postgres, and a signup count that is never faked.

SRSerban Rusu·Oct 16, 2026·10 min read
Component guides

Embla Carousel React: Accessible Slides, No Layout Shift

Build an embla carousel react component for photos, listings, testimonials and onboarding, with keyboard controls, pausable autoplay and no layout shift.

SRSerban Rusu·Oct 19, 2026·9 min read
Component guides

React Animated Number Counter: Rolling Odometer Digits

Build a React animated number counter that rolls each digit like an odometer, takes its separators from Intl, holds its width and respects reduced motion.

SRSerban Rusu·Oct 19, 2026·9 min read
Newsletter

Get new posts by email

New guides, tutorials and comparisons from the Wingo UI blog, sent when they are published.

No spam. Unsubscribe at any time.

WingoUI

Animated, configurable, mobile-first React components and blocks. Copy the source, make it yours, and let your coding agent build with it.

Product

  • Components
  • Templates
  • Pricing
  • AI agents
  • Theme generator

Resources

  • Docs
  • Blog
  • Changelog
  • Changelog RSS
  • llms.txt

Legal

  • Terms
  • Privacy
  • Refunds
  • License
  • Imprint

Component categories

  • Buttons & Actions
  • Inputs
  • Forms
  • Navigation
  • Overlays
  • Feedback
  • Data Display
  • Tables & Lists
  • Charts & Stats
  • Layout
  • Media
  • AI Kit
  • Text & Effects
  • Mobile
  • Commerce
  • Marketing Sections
  • Blocks
  • Hooks & Utilities

© 2026 Wingo UI

Wingo UI