Countdown Timer Implementation for Website

Our company is engaged in the development, support and maintenance of sites of any complexity. From simple one-page sites to large-scale cluster systems built on micro services. Experience of developers is confirmed by certificates from vendors.
Development and maintenance of all types of websites:
Informational websites or web applications
Business card websites, landing pages, corporate websites, online catalogs, quizzes, promo websites, blogs, news resources, informational portals, forums, aggregators
E-commerce websites or web applications
Online stores, B2B portals, marketplaces, online exchanges, cashback websites, exchanges, dropshipping platforms, product parsers
Business process management web applications
CRM systems, ERP systems, corporate portals, production management systems, information parsers
Electronic service websites or web applications
Classified ads platforms, online schools, online cinemas, website builders, portals for electronic services, video hosting platforms, thematic portals

These are just some of the technical types of websites we work with, and each of them can have its own specific features and functionality, as well as be customized to meet the specific needs and goals of the client.

Our competencies:
Development stages
Latest works
  • image_website-b2b-advance_0.png
    B2B ADVANCE company website development
    1212
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1161
  • image_websites_belfingroup_462_0.webp
    Website development for BELFINGROUP
    852
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1041
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    822
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Website development for FIXPER company
    815

Implementing Countdown Timer on Website

Countdown timer is a marketing tool for promotions, launches, registrations. Technically simple, but has nuances with time zones, SEO, and client-side manipulations.

Basic JavaScript Implementation

function createCountdown(targetDate: Date, container: HTMLElement) {
  function update() {
    const now = Date.now()
    const diff = targetDate.getTime() - now

    if (diff <= 0) {
      container.innerHTML = '<span class="countdown__ended">Promotion ended</span>'
      return
    }

    const days = Math.floor(diff / (1000 * 60 * 60 * 24))
    const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60))
    const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60))
    const seconds = Math.floor((diff % (1000 * 60)) / 1000)

    container.innerHTML = `
      <div class="countdown">
        ${days > 0 ? `<div class="countdown__unit">
          <span class="countdown__value">${String(days).padStart(2, '0')}</span>
          <span class="countdown__label">days</span>
        </div>` : ''}
        <div class="countdown__unit">
          <span class="countdown__value">${String(hours).padStart(2, '0')}</span>
          <span class="countdown__label">hours</span>
        </div>
        <div class="countdown__unit">
          <span class="countdown__value">${String(minutes).padStart(2, '0')}</span>
          <span class="countdown__label">minutes</span>
        </div>
        <div class="countdown__unit">
          <span class="countdown__value">${String(seconds).padStart(2, '0')}</span>
          <span class="countdown__label">seconds</span>
        </div>
      </div>
    `
  }

  update()
  const interval = setInterval(update, 1000)
  return () => clearInterval(interval)  // return cleanup function
}

// Initialization: date from data-attribute
document.querySelectorAll('[data-countdown]').forEach(el => {
  const target = new Date(el.getAttribute('data-countdown')!)
  createCountdown(target, el as HTMLElement)
})

React Component with Flip Animation

import { useState, useEffect, useRef } from 'react'

function useCountdown(targetDate: Date) {
  const [timeLeft, setTimeLeft] = useState(() => getTimeLeft(targetDate))

  useEffect(() => {
    const tick = () => setTimeLeft(getTimeLeft(targetDate))
    tick()
    const id = setInterval(tick, 1000)
    return () => clearInterval(id)
  }, [targetDate])

  return timeLeft
}

function getTimeLeft(target: Date) {
  const diff = Math.max(0, target.getTime() - Date.now())
  return {
    days: Math.floor(diff / 86400000),
    hours: Math.floor((diff % 86400000) / 3600000),
    minutes: Math.floor((diff % 3600000) / 60000),
    seconds: Math.floor((diff % 60000) / 1000),
    expired: diff === 0,
  }
}

function FlipUnit({ value, label }: { value: number; label: string }) {
  const [flip, setFlip] = useState(false)
  const prevValue = useRef(value)

  useEffect(() => {
    if (prevValue.current !== value) {
      setFlip(true)
      prevValue.current = value
      const t = setTimeout(() => setFlip(false), 300)
      return () => clearTimeout(t)
    }
  }, [value])

  return (
    <div className="flip-unit">
      <div className={`flip-unit__card ${flip ? 'flip-unit__card--flip' : ''}`}>
        <span className="flip-unit__value">{String(value).padStart(2, '0')}</span>
      </div>
      <span className="flip-unit__label">{label}</span>
    </div>
  )
}

export function CountdownTimer({ target, onExpire }: { target: Date; onExpire?: () => void }) {
  const { days, hours, minutes, seconds, expired } = useCountdown(target)

  useEffect(() => {
    if (expired) onExpire?.()
  }, [expired, onExpire])

  if (expired) return <div className="countdown--expired">Time's up</div>

  return (
    <div className="countdown-timer" role="timer" aria-label="Countdown">
      {days > 0 && <FlipUnit value={days} label="days" />}
      <FlipUnit value={hours} label="hours" />
      <FlipUnit value={minutes} label="minutes" />
      <FlipUnit value={seconds} label="seconds" />
    </div>
  )
}
.flip-unit__card {
  position: relative;
  display: inline-block;
  transition: transform 0.3s ease;
  transform-style: preserve-3d;
}

.flip-unit__card--flip {
  animation: flip 0.3s ease;
}

@keyframes flip {
  0%   { transform: rotateX(0deg); }
  50%  { transform: rotateX(-90deg); }
  100% { transform: rotateX(0deg); }
}

Time Zones: Proper Handling

Common mistake — store event date without timezone. User from one timezone sees promo 7 hours later than user from another.

// On server — always UTC
// In Laravel:
$event->ends_at = Carbon::parse('2025-06-01 23:59:59', 'Europe/London')->utc();

// On client — get UTC ISO string
const targetUTC = '2025-06-01T20:59:59Z'  // already UTC
const target = new Date(targetUTC)          // JS automatically converts to local time

If need to show same time for everyone (e.g., "sale ends at midnight in London"):

import { zonedTimeToUtc } from 'date-fns-tz'

const londonMidnight = zonedTimeToUtc('2025-06-01 00:00:00', 'Europe/London')
// All users count down to this UTC moment

Protection from DevTools Manipulation

Opening DevTools and changing system time or tweaking JS variable is trivial. If timer is tied to business logic (discount, access), validation must be on server:

// Middleware for checking promo activity
class PromoActive
{
    public function handle(Request $request, Closure $next): Response
    {
        $promo = Promo::findOrFail($request->route('promo'));

        if (!$promo->isActive()) {
            return response()->json(['error' => 'Promotion ended'], 410);
        }

        return $next($request);
    }
}

Timer with Server Time (Protection from Desync)

async function getServerTimeDelta(): Promise<number> {
  const t0 = Date.now()
  const response = await fetch('/api/time')
  const t1 = Date.now()
  const serverTime: number = await response.json()

  // Compensate half of round-trip time
  const delta = serverTime - ((t0 + t1) / 2)
  return delta
}

// Use at start
const delta = await getServerTimeDelta()
const getAdjustedNow = () => Date.now() + delta

Evergreen Timer (Restarts for Each User)

Pattern: promotion lasts 24 hours from first visit. Stored in localStorage/cookie:

function getOrCreateDeadline(durationMs: number): Date {
  const key = 'promo_deadline'
  const stored = localStorage.getItem(key)

  if (stored) {
    const deadline = new Date(stored)
    if (deadline > new Date()) return deadline
  }

  const deadline = new Date(Date.now() + durationMs)
  localStorage.setItem(key, deadline.toISOString())
  return deadline
}

const deadline = getOrCreateDeadline(24 * 60 * 60 * 1000)  // 24 hours

SEO: Completion Time for Search Engines

<!-- Schema.org for timed events -->
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "SaleEvent",
  "name": "Summer Sale",
  "startDate": "2025-06-01T00:00:00+01:00",
  "endDate": "2025-06-07T23:59:59+01:00",
  "offers": {
    "@type": "Offer",
    "discount": "50%"
  }
}
</script>

Timeline

Static timer with basic layout — 2–3 hours. With flip animation, proper time zones, and responsive design — 1 day. With server sync, evergreen logic, and Schema.org markup — 1.5 days.