Skip to content

Countdown

SM · 950B gzip budget

A live countdown with locale-correct digits.

Category
Feedback & status
Budget
sm, single-purpose control
External runtime
React / React DOM only
axe fixtureSSR rendersource scanbundle budget

Preview

Live fixture
Preview locale
01:00

Try the component here. Locale-aware formatting and direction follow the selected language; example content and labels are not automatically translated. Overlay demos use interactive launchers.

Install

npx shadcn@latest add https://gear5-ui.vercel.app/r/countdown.json

Copies the source into your project. Pulls in 2 shared registry items: use-locale, locale.

New here? Set up Tailwind and import aliases first →

Source & props

Edit on GitHub ↗

Prop types and inline documentation are included below. This is repository source; the installer rewrites shared imports for your project.

"use client";

import { useEffect, useState } from "react";
import { useLocale } from "../lib/use-locale";

export interface CountdownProps {
  target: Date | number;
  /** Called once when the countdown reaches zero. */
  onComplete?: () => void;
  className?: string;
}

function remaining(target: Date | number): number {
  return Math.max(0, new Date(target).getTime() - Date.now());
}

/**
 * A live countdown with digits formatted through `Intl.NumberFormat`, so a
 * reader using Arabic-Indic or Devanagari numerals sees their own digits, not
 * Western ones dropped into an otherwise-translated page. Updates once a
 * second — no animation, so there is nothing here for reduced-motion to
 * disable.
 */
export function Countdown({ target, onComplete, className }: CountdownProps) {
  const { locale } = useLocale();
  const [ms, setMs] = useState(() => remaining(target));

  useEffect(() => {
    if (ms <= 0) {
      onComplete?.();
      return;
    }
    const timer = setInterval(() => setMs(remaining(target)), 1000);
    return () => clearInterval(timer);
  }, [target, ms, onComplete]);

  const totalSeconds = Math.ceil(ms / 1000);
  const hours = Math.floor(totalSeconds / 3600);
  const minutes = Math.floor((totalSeconds % 3600) / 60);
  const seconds = totalSeconds % 60;
  const format = (n: number) => new Intl.NumberFormat(locale, { minimumIntegerDigits: 2 }).format(n);

  return (
    <span role="timer" aria-live="off" className={className}>
      {hours > 0 && `${format(hours)}:`}
      {format(minutes)}:{format(seconds)}
    </span>
  );
}

What CI checks

Quality contract
  • Bundled, minified and gzipped against its tier budget
  • Audited by axe in the state previewed above
  • Rendered through react-dom/server with no browser globals
  • Scanned for network calls, dangerous sinks, and unguarded animation