Перейти к содержанию

Скрипт обратного отсчёта времени до конца акции

Инструкция
Как поставить на сайт таймер обратного отсчёта до конца акции на чистом JavaScript, без библиотек. Разбираем надёжный разбор даты, счёт от текущего момента, склонение единиц и остановку по нулю.

7 мин 63 Обновлено 22 июля Сложность: Средняя

TL;DR

  • Дату окончания храните в data-атрибуте в полном ISO-формате с часовым поясом (2026-05-31T23:59:59+03:00).
  • Короткая дата 2026-05-31 трактуется как полночь UTC, а формат с пробелом ломался в старом Safari.
  • Таймер пишется на чистом JavaScript без jQuery и внешних библиотек.
  • Каждый тик считает разницу от Date.now(), поэтому счётчик не накапливает ошибку.
  • На нуле выводим «Акция завершена» и вызываем clearInterval, чтобы не уйти в минус.

Баннер акции с таймером «до конца распродажи осталось 2 дня 05:14:30» подгоняет к покупке и не требует ни jQuery, ни внешних библиотек. Достаточно одного блока разметки и десятка строк на современном JavaScript. Разберём рабочий вариант и починим типичные ошибки старых сниппетов.

Разметка: где хранить дату окончания

Дату конца акции удобно держать прямо в разметке – в data-атрибуте блока. Формат ISO 8601 с указанием времени и часового пояса разбирается всеми браузерами одинаково, поэтому лучше писать полную дату, а не короткую 2026-05-31:

<div id="deal-countdown" class="countdown"
     data-deadline="2026-05-31T23:59:59+03:00"></div>

Почему это важно: строку "2026-05-31" (только дата) стандарт велит трактовать как полночь UTC, а "2026-05-31 23:59:59" (с пробелом вместо T) старые версии Safari вообще отказывались разбирать и возвращали Invalid Date. Полный ISO-формат с поясом снимает обе проблемы.

Скрипт таймера на чистом JavaScript

Забираем дедлайн из атрибута, раз в секунду считаем разницу с текущим моментом и обновляем текст. Современный синтаксис (const/let, стрелочные функции, шаблонные строки) делает код короче и понятнее старого варианта на var:

const box = document.getElementById('deal-countdown');

if (box) {
  const deadline = new Date(box.dataset.deadline).getTime();

  const plural = (n, forms) => {
    const idx = (n % 100 > 4 && n % 100 < 20)
      ? 2
      : [2, 0, 1, 1, 1, 2][Math.min(n % 10, 5)];
    return forms[idx];
  };

  const pad = (n) => String(n).padStart(2, '0');

  const render = () => {
    const diff = deadline - Date.now();

    if (diff <= 0) {
      box.textContent = 'Акция завершена';
      clearInterval(timer);
      return;
    }

    const totalSec = Math.floor(diff / 1000);
    const days = Math.floor(totalSec / 86400);
    const hours = Math.floor((totalSec % 86400) / 3600);
    const mins = Math.floor((totalSec % 3600) / 60);
    const secs = totalSec % 60;

    box.textContent =
      `${days} ${plural(days, ['день', 'дня', 'дней'])} ` +
      `${pad(hours)}:${pad(mins)}:${pad(secs)}`;
  };

  render();                       // сразу, без задержки в секунду
  const timer = setInterval(render, 1000);
}

Что здесь исправлено по сравнению со старым сниппетом

  • Отрицательные значения. Старый код продолжал считать после окончания акции и показывал минусы. Теперь при diff <= 0 выводится «Акция завершена» и интервал останавливается через clearInterval.
  • Мгновенный старт. Прежний таймер целую секунду висел пустым, пока не сработает первый setInterval. Мы вызываем render() сразу.
  • Округление. parseInt заменён на Math.floor – это семантически правильнее для отбрасывания дробной части и не зависит от строкового приведения.
  • Русские подписи и склонение. Вместо «Days/Hours» – «день/дня/дней» с корректной формой, а часы/минуты/секунды выводятся в привычном формате чч:мм:сс с ведущими нулями (padStart).
  • Проверка наличия блока. if (box) защищает от ошибки на страницах, где баннера нет.

Почему счёт всегда от Date.now()

Каждый тик берёт свежую разницу «дедлайн минус текущий момент», а не вычитает секунду из прошлого значения. Поэтому таймер не накапливает ошибку и остаётся точным, даже если вкладку свернули, а браузер придержал таймеры для экономии батареи – вернувшись, пользователь увидит верное время.

Подводные камни

  • Часовой пояс дедлайна. Указывайте пояс прямо в дате (+03:00). Иначе «полночь» для клиента в Калининграде и во Владивостоке наступит в разные моменты, и акция закончится у всех по-разному.
  • Клиентские часы врут. Таймер опирается на время устройства посетителя. Если критична точность до секунды (аукцион, флеш-распродажа), сверяйтесь с серверным временем, а не только с Date.now().
  • Несколько таймеров. Скрипт написан под один блок по id. Для нескольких акций используйте класс и querySelectorAll(...).forEach(...), читая дедлайн из data-атрибута каждого.
  • Кэш страницы. Дедлайн в разметке кэшируется вместе со страницей – это нормально: сам отсчёт живой, потому что считается на клиенте. Менять дату акции нужно в исходнике, а не рассчитывать её на сервере при каждом заходе.

Вариант этого таймера, где дата берётся из свойства элемента инфоблока, – в гайде «Счётчик обратного отсчёта в списке новостей». Про работу с датами на стороне сервера – «Сменить формат даты в CMS Битрикс», «Формат Ydiff» и «Вывод текущего года на PHP».

Частые вопросы

Нужна ли библиотека для таймера обратного отсчёта?

Нет. Таймер до конца акции пишется на чистом JavaScript в десяток строк с setInterval и Date.now(). jQuery и специальные плагины для этого не нужны.

В каком формате указывать дату окончания акции?

Используйте полный ISO 8601 с часовым поясом, например 2026-05-31T23:59:59+03:00. Короткая дата «2026-05-31» трактуется как полночь UTC, а формат с пробелом раньше ломался в Safari.

Почему таймер после окончания показывает отрицательные числа?

Не остановлен интервал. Когда разница deadline − Date.now() становится

Почему таймер сбивается после сворачивания вкладки?

Он не сбивается, если каждый тик считает свежую разницу от Date.now(), а не вычитает секунду из прошлого значения. Браузер может придержать таймеры свёрнутой вкладки, но при возврате пересчёт покажет верное время.

Как поставить несколько таймеров на одной странице?

Задайте блокам общий класс и дедлайн в data-атрибуте, затем переберите их через querySelectorAll(...).forEach(...), создав для каждого свой отсчёт.

Материалы по теме

Что почитать дальше по этой теме

Инструкция Счётчик обратного отсчёта в «Списке новостей» Битрикса Как вывести ближайшее мероприятие компонентом «Список новостей» и повесить на него таймер обратного отсчёта, взяв дату из свойства элемента. Дату отдаём в JS через data-атрибут, а сам счётчик пишем на чистом JavaScript без jQuery. 8 мин 44 22 октября 2017 Инструкция Сменить формат даты в CMS «Битрикс» Как вывести поле «Начало активности» элемента инфоблока в формате «02 июня, 2016». Разбираем FormatDate с шаблоном «d F, Y», разбор строки через ParseDateTime и подводные камни с локалью и часовым поясом. 6 мин 50 18 мая 2017 Инструкция Как отформатировать дату в формате Ydiff (например, «2 года») Как вывести разницу между датой и сегодняшним днём словами – «2 года», «6 месяцев», «3 дня». Разбираем относительный формат FormatDate «Ydiff» в Битрикс, автоматическое склонение и как разбить результат на число и слово. 6 мин 54 18 июля 2019 Инструкция Вывод текущего года на PHP (копирайт в подвале) Как автоматически выводить в подвале сайта текущий год или интервал «2022–2026», чтобы копирайт не устаревал 1 января. Разбираем date('Y'), тернарный оператор, часовой пояс и вариант для 1С-Битрикс. 5 мин 69 22 сентября 2022 Инструкция Уведомление о cookies на сайт Как добавить на сайт уведомление о cookies с учётом 152-ФЗ: согласие на обработку персональных данных, ссылка на политику, доступный баннер и подключение Яндекс.Метрики только после согласия. Готовый код без устаревших зависимостей. 8 мин 55 16 сентября 2022