Countdown
countdown ui Countdown timer that shows time remaining to a target date. Supports DD:HH:MM:SS, HH:MM:SS, MM:SS, and SS formats plus custom token formats, render props for days/hours/minutes/seconds, a label, leading-zero padding, a custom separator, paused state, and on-finish/tick events.
Also available for Vue ->Installation
$ pnpm dlx shadcn@latest add https://uipkge.dev/r/react/countdown.json $ npx shadcn@latest add https://uipkge.dev/r/react/countdown.json $ yarn dlx shadcn@latest add https://uipkge.dev/r/react/countdown.json $ bunx shadcn@latest add https://uipkge.dev/r/react/countdown.json Named registry:
npx shadcn@latest add @uipkge-react/countdown Installs to: components/ui/countdown/ Examples
Props
| Name | Type / Values | Default | Required |
|---|---|---|---|
days | number | — | required |
hours | number | — | required |
minutes | number | — | required |
seconds | number | — | required |
display | string | — | required |
finished | boolean | — | required |
Files installed (2)
-
components/ui/countdown/Countdown.tsx 6.8 kB
import * as React from 'react' import { cn } from '@/lib/utils' type Format = 'DD:HH:MM:SS' | 'HH:MM:SS' | 'MM:SS' | 'SS' export interface CountdownRenderProps { days: number hours: number minutes: number seconds: number display: string finished: boolean } export interface CountdownProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'children'> { /** Target date/time. Accepts a Date, ISO string, or epoch ms number. */ target: Date | string | number /** Display format. Custom tokens: DD days, HH hours, MM minutes, SS seconds. */ format?: Format | string /** Pause the countdown. */ paused?: boolean /** Optional label rendered above the countdown. */ label?: string /** Show leading zeros (e.g. 05 vs 5). */ pad?: boolean /** Separator between units. */ separator?: string /** Fired once when the countdown reaches zero. */ onFinish?: () => void /** Fired every tick with the remaining ms. */ onTick?: (remaining: number) => void /** Render-prop override for the whole display. Receives all parts + display string. */ children?: ((props: CountdownRenderProps) => React.ReactNode) | React.ReactNode /** Render-prop override for the days unit. */ renderDays?: (days: number) => React.ReactNode /** Render-prop override for the hours unit. */ renderHours?: (hours: number) => React.ReactNode /** Render-prop override for the minutes unit. */ renderMinutes?: (minutes: number) => React.ReactNode /** Render-prop override for the seconds unit. */ renderSeconds?: (seconds: number) => React.ReactNode } const Countdown = React.forwardRef<HTMLDivElement, CountdownProps>( ( { target, format = 'DD:HH:MM:SS', paused = false, label = '', pad = true, separator = ':', onFinish, onTick, children, renderDays, renderHours, renderMinutes, renderSeconds, className, ...props }, ref, ) => { const [now, setNow] = React.useState(() => Date.now()) const [finished, setFinished] = React.useState(false) const targetMs = React.useMemo(() => { if (target instanceof Date) return target.getTime() if (typeof target === 'number') return target return new Date(target).getTime() }, [target]) const remainingMs = Math.max(0, targetMs - now) const parts = React.useMemo(() => { const total = remainingMs const days = Math.floor(total / 86_400_000) const hours = Math.floor((total % 86_400_000) / 3_600_000) const minutes = Math.floor((total % 3_600_000) / 60_000) const seconds = Math.floor((total % 60_000) / 1000) return { days, hours, minutes, seconds } }, [remainingMs]) const pad2 = React.useCallback((n: number) => (pad ? String(n).padStart(2, '0') : String(n)), [pad]) const display = React.useMemo(() => { const f = format const { days, hours, minutes, seconds } = parts const sep = separator if (f === 'DD:HH:MM:SS') return `${pad2(days)}${sep}${pad2(hours)}${sep}${pad2(minutes)}${sep}${pad2(seconds)}` if (f === 'HH:MM:SS') return `${pad2(days * 24 + hours)}${sep}${pad2(minutes)}${sep}${pad2(seconds)}` if (f === 'MM:SS') return `${pad2(days * 24 * 60 + hours * 60 + minutes)}${sep}${pad2(seconds)}` if (f === 'SS') return pad2(Math.floor(remainingMs / 1000)) // Custom token format: replace DD, HH, MM, SS tokens. return f .replace('DD', pad2(days)) .replace('HH', pad2(hours)) .replace('MM', pad2(minutes)) .replace('SS', pad2(seconds)) }, [format, parts, separator, pad2, remainingMs]) // Keep latest callbacks without retriggering the interval effect. const onFinishRef = React.useRef(onFinish) const onTickRef = React.useRef(onTick) onFinishRef.current = onFinish onTickRef.current = onTick React.useEffect(() => { if (paused) return const timer = setInterval(() => { const next = Date.now() const remaining = Math.max(0, targetMs - next) onTickRef.current?.(remaining) if (remaining <= 0) { setFinished(true) onFinishRef.current?.() } setNow(next) }, 1000) return () => clearInterval(timer) }, [paused, targetMs]) // Reset finished state when the target changes. React.useEffect(() => { setFinished(false) setNow(Date.now()) }, [targetMs]) const renderProps: CountdownRenderProps = { ...parts, display, finished, } const renderDefault = () => { const f = format return ( <> {f.includes('DD') && (renderDays ? ( renderDays(parts.days) ) : ( <span data-slot="countdown-days" className="text-foreground text-2xl font-semibold"> {pad2(parts.days)} </span> ))} {f.includes('DD') && f.includes('HH') && <span className="text-muted-foreground text-2xl">{separator}</span>} {f.includes('HH') && (renderHours ? ( renderHours(parts.hours) ) : ( <span data-slot="countdown-hours" className="text-foreground text-2xl font-semibold"> {pad2(parts.hours)} </span> ))} {f.includes('HH') && f.includes('MM') && <span className="text-muted-foreground text-2xl">{separator}</span>} {f.includes('MM') && (renderMinutes ? ( renderMinutes(parts.minutes) ) : ( <span data-slot="countdown-minutes" className="text-foreground text-2xl font-semibold"> {pad2(parts.minutes)} </span> ))} {f.includes('MM') && f.includes('SS') && <span className="text-muted-foreground text-2xl">{separator}</span>} {f.includes('SS') && (renderSeconds ? ( renderSeconds(parts.seconds) ) : ( <span data-slot="countdown-seconds" className="text-foreground text-2xl font-semibold"> {pad2(parts.seconds)} </span> ))} </> ) } return ( <div ref={ref} data-uipkge="" data-slot="countdown" data-finished={finished} data-paused={paused} className={cn('inline-flex flex-col gap-1', className)} {...props} > {label && ( <span data-slot="countdown-label" className="text-muted-foreground text-xs font-medium tracking-wide uppercase" > {label} </span> )} <div data-slot="countdown-display" className="flex items-baseline gap-1 font-mono tabular-nums"> {typeof children === 'function' ? children(renderProps) : (children ?? renderDefault())} </div> </div> ) }, ) Countdown.displayName = 'Countdown' export { Countdown } -
components/ui/countdown/index.ts 0.1 kB
export { Countdown, type CountdownProps, type CountdownRenderProps } from './Countdown'
Raw manifest: https://uipkge.dev/r/react/countdown.json