Реализация таймера обратного отсчёта на сайте

Наша компания занимается разработкой, поддержкой и обслуживанием сайтов любой сложности. От простых одностраничных сайтов до масштабных кластерных систем построенных на микро сервисах. Опыт разработчиков подтвержден сертификатами от вендоров.

Разработка и обслуживание любых видов сайтов:

Информационные сайты или веб-приложения
Сайты визитки, 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">${pluralize(days, ['день', 'дня', 'дней'])}</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)  // возвращаем функцию очистки
}

function pluralize(n: number, forms: [string, string, string]): string {
  const mod10 = n % 10
  const mod100 = n % 100
  if (mod10 === 1 && mod100 !== 11) return forms[0]
  if (mod10 >= 2 && mod10 <= 4 && (mod100 < 10 || mod100 >= 20)) return forms[1]
  return forms[2]
}

// Инициализация: дата из 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); }
}

Часовые пояса: правильная обработка

Распространённая ошибка — хранить дату акции без таймзоны. Пользователь из Владивостока видит акцию на 7 часов позже, чем из Москвы.

// На сервере — всегда UTC
// В Laravel:
$event->ends_at = Carbon::parse('2025-06-01 23:59:59', 'Europe/Moscow')->utc();

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

Если нужно показывать одно и то же время для всех (например, «распродажа кончается в полночь по Москве»):

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

const moscowMidnight = zonedTimeToUtc('2025-06-01 00:00:00', 'Europe/Moscow')
// Все пользователи считают время до этого 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 дня.