Реалізація таймера зворотного відліку на сайті

Наша компанія займається розробкою, підтримкою та обслуговуванням сайтів будь-якої складності. Від простих односторінкових сайтів до масштабних кластерних систем, побудованих на мікро сервісах. Досвід розробників підтверджено сертифікатами від вендорів.

Розробка та обслуговування будь-яких видів сайтів:

Інформаційні сайти або веб-програми
Сайти візитки, landing page, корпоративні сайти, онлайн каталоги, квіз, промо-сайти, блоги, ресурси новин, інформаційні портали, форуми, агрегатори
Сайти або веб-програми електронної комерції
Інтернет-магазини, B2B-портали, маркетплейси, онлайн-обмінники, кешбек-сайти, біржі, дропшиппінг-платформи, парсери товарів
Веб-програми для управління бізнес-процесами
CRM-системи, ERP-системи, корпоративні портали, системи управління виробництвом, парсери інформації
Сайти або веб-програми електронних послуг
Дошки оголошень, онлайн-школи, онлайн-кінотеатри, конструктори сайтів, портали надання електронних послуг, відеохостинги, тематичні портали

Це лише деякі з технічних типів сайтів, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Пропоновані послуги
Показано 1 з 1 послугУсі 2065 послуг
Реалізація таймера зворотного відліку на сайті
Проста
від 4 годин до 2 робочих днів
Часті питання

Наші компетенції:

Етапи розробки

Останні роботи

  • image_website-b2b-advance_0.png
    Розробка сайту компанії B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    874
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Розробка веб-сайту для компанії ФІКСПЕР
    851

Реалізація таймера зворотного відліку на вебсайті

Таймер зворотного відліку — маркетинговий інструмент для акцій, запусків, реєстрацій. Технічно простий, але є нюанси з часовими поясами, SEO та маніпуляціями на клієнті.

Базова реалізація на JavaScript

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">Акція завершена</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">днів</span>
        </div>` : ''}
        <div class="countdown__unit">
          <span class="countdown__value">${String(hours).padStart(2, '0')}</span>
          <span class="countdown__label">годин</span>
        </div>
        <div class="countdown__unit">
          <span class="countdown__value">${String(minutes).padStart(2, '0')}</span>
          <span class="countdown__label">хвилин</span>
        </div>
        <div class="countdown__unit">
          <span class="countdown__value">${String(seconds).padStart(2, '0')}</span>
          <span class="countdown__label">секунд</span>
        </div>
      </div>
    `
  }

  update()
  const interval = setInterval(update, 1000)
  return () => clearInterval(interval)  // повертаємо функцію очистки
}

// Ініціалізація: дата з data-атрибута
document.querySelectorAll('[data-countdown]').forEach(el => {
  const target = new Date(el.getAttribute('data-countdown')!)
  createCountdown(target, el as HTMLElement)
})

React-компонент з flip-анімацією

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">Час вийшов</div>

  return (
    <div className="countdown-timer" role="timer" aria-label="Зворотний відлік">
      {days > 0 && <FlipUnit value={days} label="днів" />}
      <FlipUnit value={hours} label="годин" />
      <FlipUnit value={minutes} label="хвилин" />
      <FlipUnit value={seconds} label="секунд" />
    </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); }
}

Часові пояси: правильна обробка

Поширена помилка — зберігати дату акції без часового поясу. Користувач з Києва бачить акцію на 3 години пізніше, ніж користувач з іншого часового поясу.

// На сервері — завжди UTC
// У Laravel:
$event->ends_at = Carbon::parse('2025-06-01 23:59:59', 'Europe/Kyiv')->utc();

// На клієнті — отримуємо UTC ISO рядок
const targetUTC = '2025-06-01T20:59:59Z'  // вже UTC
const target = new Date(targetUTC)          // JS автоматично переводить в локальний час

Якщо потрібно показувати один і той же час для всіх (наприклад, "акція закінчується в полуніч за київським часом"):

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

const kyivMidnight = zonedTimeToUtc('2025-06-01 00:00:00', 'Europe/Kyiv')
// Усі користувачі рахують час до цього UTC-моменту

Захист від маніпуляцій через DevTools

Відкрити DevTools і змінити системний час або підправити JS-змінну — тривіально. Якщо таймер пов'язаний з бізнес-логікою (знижка, доступ), перевірка має бути на сервері:

// Middleware для перевірки активності акції
class PromoActive
{
    public function handle(Request $request, Closure $next): Response
    {
        $promo = Promo::findOrFail($request->route('promo'));

        if (!$promo->isActive()) {
            return response()->json(['error' => 'Акція завершена'], 410);
        }

        return $next($request);
    }
}

Таймер з серверним часом (захист від рассинхрону)

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()

  // Компенсуємо половину round-trip часу
  const delta = serverTime - ((t0 + t1) / 2)
  return delta
}

// Використовуємо при старті
const delta = await getServerTimeDelta()
const getAdjustedNow = () => Date.now() + delta

Evergreen-таймер (перезапускається для кожного користувача)

Паттерн: акція довжиться 24 години з моменту першого візиту. Зберігається в 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 години

SEO: час завершення для пошукових систем

<!-- Schema.org для подій з таймером -->
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "SaleEvent",
  "name": "Літня розпродаж",
  "startDate": "2025-06-01T00:00:00+03:00",
  "endDate": "2025-06-07T23:59:59+03:00",
  "offers": {
    "@type": "Offer",
    "discount": "50%"
  }
}
</script>

Терміни

Статичний таймер з базовою версткою — 2–3 години. З flip-анімацією, правильними часовими поясами та адаптивом — 1 день. З серверною синхронізацією, evergreen-логікою та Schema.org розміткою — 1,5 дня.