Components

Urgency Countdown Timer

A versatile countdown timer that displays the time remaining until a specified target date.

Small

Medium (Default)

Large

⏳ Waiting for countdown to complete...

Flash Sale Ending Soon!

Get 50% off all digital products. Don't miss out!

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

PropTypeDefaultDescription
targetDateDate | string | numberRequiredThe 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.
onCompleteContentReact.ReactNode-Optional content to display when the countdown timer reaches zero.
onComplete() => void-An optional callback function to execute when the countdown completes.
childrenReact.ReactNode-Optional content, such as a call-to-action button, to display alongside the timer.