Components
Urgency Countdown Timer
A versatile countdown timer that displays the time remaining until a specified target date.
Installation
CLI
Installation via the CLI is coming soon. For now, please follow the manual installation instructions below.
Manual
Copy and paste the following code into a new file components/ui/urgency-countdown-timer.tsx
:
'use client';
import * as React from 'react';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/lib/utils';
const timeSegmentVariants = cva(
'flex flex-col items-center justify-center rounded-md bg-muted text-center',
{
variants: { size: { sm: 'w-12 p-1', md: 'w-16 p-1.5', lg: 'w-20 p-2' } },
defaultVariants: { size: 'md' },
}
);
const timeSegmentNumberVariants = cva(
'font-mono font-bold tabular-nums tracking-tighter',
{
variants: { size: { sm: 'text-lg', md: 'text-2xl', lg: 'text-4xl' } },
defaultVariants: { size: 'md' },
}
);
const timeSegmentLabelVariants = cva(
'uppercase tracking-wider text-muted-foreground',
{
variants: { size: { sm: 'text-[0.6rem]', md: 'text-xs', lg: 'text-sm' } },
defaultVariants: { size: 'md' },
}
);
const timeSeparatorVariants = cva(
'animate-pulse font-bold text-muted-foreground/70',
{
variants: {
size: { sm: 'px-0.5 text-lg', md: 'px-0.5 text-xl', lg: 'px-1 text-2xl' },
},
defaultVariants: { size: 'md' },
}
);
type SizeProp = VariantProps<typeof timeSegmentVariants>;
const TimeSegment = ({
value,
label,
size,
}: {
value: number;
label: string;
size?: SizeProp['size'];
}) => (
<div className={cn(timeSegmentVariants({ size }))}>
<span className={cn(timeSegmentNumberVariants({ size }))}>
{value.toString().padStart(2, '0')}
</span>
<span className={cn(timeSegmentLabelVariants({ size }))}>{label}</span>
</div>
);
const TimeSeparator = ({ size }: SizeProp) => (
<div aria-hidden='true' className={cn(timeSeparatorVariants({ size }))}>
:
</div>
);
/**
* Props for the UrgencyCountdownTimer component.
*/
export interface UrgencyCountdownTimerProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof timeSegmentVariants> {
/**
* The future date and time to count down to. Can be a Date object, a timestamp number, or a date string.
*/
targetDate: Date | string | number;
/**
* Optional content to display when the countdown timer reaches zero.
*/
onCompleteContent?: React.ReactNode;
/**
* An optional callback function to execute when the countdown completes.
*/
onComplete?: () => void;
}
/**
* A versatile countdown timer that displays the time remaining until a specified target date.
* It supports different sizes and provides callbacks and custom content for when the timer completes.
*/
const UrgencyCountdownTimer = React.forwardRef<
HTMLDivElement,
UrgencyCountdownTimerProps
>(
(
{
className,
targetDate,
children,
onCompleteContent,
onComplete,
size,
...props
},
ref
) => {
const [timeLeft, setTimeLeft] = React.useState<{
days: number;
hours: number;
minutes: number;
seconds: number;
} | null>(null);
const [isCompleted, setIsCompleted] = React.useState(false);
const targetTime = React.useMemo(
() => new Date(targetDate).getTime(),
[targetDate]
);
React.useEffect(() => {
const calculateTimeLeft = () => {
const difference = targetTime - new Date().getTime();
if (difference <= 0) {
setIsCompleted(true);
onComplete?.();
return null;
}
return {
days: Math.floor(difference / (1000 * 60 * 60 * 24)),
hours: Math.floor((difference / (1000 * 60 * 60)) % 24),
minutes: Math.floor((difference / 1000 / 60) % 60),
seconds: Math.floor((difference / 1000) % 60),
};
};
const initialTimeLeft = calculateTimeLeft();
if (initialTimeLeft) {
setTimeLeft(initialTimeLeft);
} else {
setIsCompleted(true);
}
const timer = setInterval(() => {
const newTimeLeft = calculateTimeLeft();
if (newTimeLeft) {
setTimeLeft(newTimeLeft);
} else {
clearInterval(timer);
}
}, 1000);
return () => clearInterval(timer);
}, [targetTime, onComplete]);
if (isCompleted) {
return (
<div
ref={ref}
role='status'
aria-live='polite'
className={cn('flex items-center justify-center gap-4', className)}
{...props}
>
{onCompleteContent}
</div>
);
}
if (!timeLeft) {
return null;
}
return (
<div
ref={ref}
className={cn(
'flex flex-wrap items-center justify-center gap-2 sm:gap-4',
className
)}
{...props}
>
<div
role='timer'
aria-live='off'
className='flex items-center justify-center'
>
{timeLeft.days > 0 && (
<>
<TimeSegment value={timeLeft.days} label='Days' size={size} />
<TimeSeparator size={size} />
</>
)}
<TimeSegment value={timeLeft.hours} label='Hrs' size={size} />
<TimeSeparator size={size} />
<TimeSegment value={timeLeft.minutes} label='Min' size={size} />
<TimeSeparator size={size} />
<TimeSegment value={timeLeft.seconds} label='Sec' size={size} />
</div>
{children}
</div>
);
}
);
UrgencyCountdownTimer.displayName = 'UrgencyCountdownTimer';
export { UrgencyCountdownTimer };
Usage
Here is a basic example of how to use the UrgencyCountdownTimer
.
import { UrgencyCountdownTimer } from '@/components/ui/urgency-countdown-timer';
export default function CountdownTimerExample() {
// Set a target date 1 day from now for the default timer
const futureDate = new Date();
futureDate.setDate(futureDate.getDate() + 1);
// Set a target date 5 seconds from now for the completion demo
const shortCountdown = new Date(Date.now() + 5000);
return (
<div className='flex flex-col items-center gap-8'>
{/* Default Usage */}
<div>
<h3 className='mb-2 text-center font-semibold'>
Default Timer (Next Day)
</h3>
<UrgencyCountdownTimer targetDate={futureDate} />
</div>
{/* Customized Usage */}
<div>
<h3 className='mb-2 text-center font-semibold'>
Large with Completion Content (5s)
</h3>
<UrgencyCountdownTimer
targetDate={shortCountdown}
size='lg'
onCompleteContent={
<div className='text-center'>
<p className='text-xl font-bold'>The wait is over!</p>
<p className='text-muted-foreground'>This event has started.</p>
</div>
}
/>
</div>
</div>
);
}
Props
Prop | Type | Default | Description |
---|---|---|---|
targetDate | Date | string | number | Required | The future date and time to count down to. Can be a Date object, a timestamp number, or a date string. |
size | "sm" | "md" | "lg" | "md" | The size of the component. |
onCompleteContent | React.ReactNode | - | Optional content to display when the countdown timer reaches zero. |
onComplete | () => void | - | An optional callback function to execute when the countdown completes. |
children | React.ReactNode | - | Optional content, such as a call-to-action button, to display alongside the timer. |