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

npx shadcn@latest add https://satisui.xyz/r/urgency-countdown-timer.json

Manual

Copy and paste the following code into a new file components/satisui/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.