Перейти к содержимому
Забрать бесплатно
Модификации выключены. Так эта страница выглядит на обычной Тильде.

Таймер в Тильде: одна строка в ядре и полсекунды из вашей кнопки

29 июля 2026 13 МИН КОМАНДА MODKOD

Блок-таймер у Тильды есть: его класс `.t-timer` прописан в ядре анимации наравне с `.t-title` и `.t-btn`. Но вся эта прописка — одна строка, и она не про отсчёт, а про то, сколько ждать перед показом. Зато у неё есть цена: таймер отодвигает кнопку на полсекунды, а на блоке с тремя кнопками порядок появления ломается совсем. Плюс разберём единственный настоящий отсчёт, который Тильда написала сама, — он считает не датой, а цифрой из разметки.

✓ ПРОВЕРЕНО 29 июля 2026 функция `t_animate__assignGroupDelay` выписана из `tilda-animation-2.0.min.js` целиком, вместе с объявлениями всех семи выборок, и по ней пересчитана арифметика задержек; `.t-timer` в этом файле — 1 вхождение при контрольном `t-animate` в 153; развилка типов элементов Zero Block снята из `t396_artboard_build`; отсчёт «товар удалён» выписан из `tilda-cart-1.1.min.js` вместе с его `setInterval`, при контрольном `t706` в 312 вхождений, и сверен с правилом кольца в `tilda-cart-1.0.min.css` (контроль `.t706` — 190) — рантайм Тильды из 114 файлов JS и 39 файлов CSS, плюс 22 сохранённые страницы

Про таймер в Тильде спрашивают двумя способами: «есть ли штатный» и «как сделать свой». Ответ на оба выясняется в одном месте — в ядре анимации, где Тильда один раз упоминает класс .t-timer. Эта строка отвечает и на «есть ли», и на «что будет, если поставить». Разберём её, потом посмотрим на единственный отсчёт, который Тильда написала сама, и на то, почему в Zero Block таймера нет и не может быть.

Короткий ответ

Блок-таймер у Тильды есть — .t-timer прописан в ядре анимации наравне с заголовком и кнопкой, — но его кода в разобранном рантайме нет: он в постраничном бандле. Единственное, что ядро с таймером делает, — отодвигает появление на полсекунды, и всё, что стоит после него, съезжает. В Zero Block типа «таймер» нет: развилка закрыта десятью значениями.

Одна строка, которая доказывает, что блок есть

Вот вся прописка таймера в ядре анимации tilda-animation-2.0.min.js. Функция t_animate__assignGroupDelay выписана целиком, вместе с объявлениями выборок — без них порядок не читается:

function t_animate__assignGroupDelay(t, e) {
  var a = 0;
  if (t.querySelectorAll("[data-animate-order]").length)
    t_animate__assignOrderedElemsDelay(t, e);
  else {
    var n = t.querySelectorAll(".t-img.t-animate"),
        i = t.querySelectorAll(".t-uptitle.t-animate"),
        r = t.querySelectorAll(".t-title.t-animate");
    r = Array.prototype.filter.call(r, function (t) {
      return !t.classList.contains("t-section__title") });
    var o = t.querySelectorAll(".t-descr.t-animate");
    o = Array.prototype.filter.call(o, function (t) {
      return !t.classList.contains("t-section__descr") });
    var s = t.querySelectorAll(".t-btn.t-animate, .t-btnwrapper.t-animate");
    s = Array.prototype.filter.call(s, function (t) {
      return !t.closest(".t-section__bottomwrapper") });
    var l = t.querySelectorAll(".t-timer.t-animate"),
        m = t.querySelectorAll("form.t-animate");
    n.length && (a = .5),
    r.length && …(r → transitionDelay = a), r.length && (a += .3),
    o.length && …(o → transitionDelay = a), o.length && (a += .3),
    i.length && …(i → transitionDelay = a), i.length && (a += .3),
    (i.length || r.length || o.length) && (a += .2),
    l.length && …(l → transitionDelay = a), l.length && (a += .5),
    s.length && (s[0].style.transitionDelay = a + "s"),
    2 === s.length && (a += .4),
    2 === s.length && (s[1].style.transitionDelay = a + "s"),
    0 !== m.length && …(m → transitionDelay = a)
  }
}

Что тут важно. .t-timer стоит в одном ряду с .t-img, .t-uptitle, .t-title, .t-descr, .t-btn и form — то есть ядро Тильды знает таймер как полноправный элемент содержимого блока, ровно как заголовок. Так не пишут про чужой класс. Это и есть доказательство, что штатный блок-таймер у Тильды существует.

И одновременно это всё, что ядро с таймером делает: присваивает ему style.transitionDelay. Ни даты, ни отсчёта, ни поведения при нуле, ни циклов — в этом файле про них нет ничего. t-timer встречается в нём ровно один раз при контрольных 153 вхождениях t-animate.

Цена таймера в очереди появления: полсекунды

Очередь читается по приращениям a. Порядок такой: заголовок → описание → надзаголовок → таймер → первая кнопка → вторая кнопка → форма. Стоимость каждого шага: 0.3 с у заголовка, описания и надзаголовка, потом общая надбавка 0.2 с, если был хоть какой-то текст, 0.5 с у таймера, 0.4 с между двумя кнопками.

Полсекунды — самый крупный единичный шаг в цепочке. Столько же стоит только наличие картинки.

Посчитаем блок «картинка + заголовок + описание + таймер + две кнопки»:

элементзадержка
картинка0
заголовок0.5 с
описание0.8 с
таймер1.3 с
первая кнопка1.8 с
вторая кнопка2.2 с

Без таймера первая кнопка приехала бы на 1.3 с, вторая на 1.7 с. То есть на промо-блоке, где таймер и стоит, кнопка — та самая, на которую и рассчитывают жать, — приезжает последней и с задержкой больше двух секунд. Причём это только задержка: само проявление идёт сверху, своей длительностью.

Картинке задержка не назначается вообще. Обратите внимание: n.length && (a = .5) — наличие картинки только сдвигает старт для всех остальных, но сама выборка n ни разу не перебирается. Картинка появляется без задержки, первой, всегда.

Надзаголовок появляется после описания. Он стоит выше заголовка визуально, а задержку получает третьим — после заголовка и описания. Это не опечатка в коде, это порядок вычислений: r, потом o, потом i.

Три кнопки ломают порядок

Здесь в коде настоящая ловушка, и её видно только если посмотреть на условия:

s.length && (s[0].style.transitionDelay = a + "s"),
2 === s.length && (a += .4),
2 === s.length && (s[1].style.transitionDelay = a + "s"),

Задержку получает s[0] — всегда. И s[1] — только когда кнопок ровно две. Если кнопок три и больше, вторая и третья не получают задержки вообще: у них остаётся то, что дал CSS, то есть ноль. Итог: на блоке с тремя кнопками первая кнопка выезжает последней, через 1.8 секунды, а вторая и третья — сразу, вместе с картинкой. Порядок вывернут наизнанку.

Обходится это переключением на ручной порядок: если внутри блока есть хоть один элемент с data-animate-order, вся функция уходит в другую ветку — t_animate__assignOrderedElemsDelay, — и цепочка по классам не применяется совсем.

Единственный настоящий отсчёт, который Тильда написала сама

Он живёт в корзине T706 и считает не до даты, а до нуля: это плашка «товар удалён, вернуть». Вот её setInterval, из tilda-cart-1.1.min.js:

var m = setInterval(function () {
  var t = l.querySelector(".t706__product-deleted__timer__counter__number"),
      e = t.innerText, o;
  window.tcart_fullscreen
    ? document.body.classList.contains("t706__body_cartsidebarshowed")
      ? o = !document.querySelector(".t706__sidebar")
              .classList.contains("t706__sidebar_showed")
      : document.body.classList.contains("t706__body_cartpageshowed") &&
        (o = !document.querySelector(".t706__cartpage")
               .classList.contains("t706__cartpage_showed"))
    : o = !document.querySelector(".t706__cartwin")
             .classList.contains("t706__cartwin_showed"),
  o && clearInterval(m),
  e > 1
    ? t.innerText = parseInt(e, 10) - 1
    : (clearInterval(m), tcart_fadeOut(l, 200, function () { … }))
}, 1e3);

Три свойства, которые стоит перенять и одно — которое стоит не перенимать.

Считает цифрой из разметки, а не датой. e = t.innerText, потом t.innerText = parseInt(e, 10) - 1. Число живёт в DOM, интервал его уменьшает. Такой счётчик не знает, сколько времени прошло на самом деле: если браузер придушил интервал — фоновая вкладка, слабый телефон, тяжёлый скрипт рядом — секунды теряются и не догоняются. Ровно так и рождается «у всех показывает разное». Свой таймер надо считать от Date.now(), а не от предыдущего значения.

Скрыт — значит стоп. Каждый тик проверяет, на месте ли класс _showed у контейнера корзины, и если контейнер спрятан, интервал убивает себя. Это соглашение самой Тильды, и оно разумное: считать в невидимом блоке незачем.

Но проверка стоит без выхода. o && clearInterval(m) — и дальше, без return, всё равно исполняется e > 1 ? …. То есть на том самом тике, когда контейнер уже скрыт, цифра уменьшается ещё раз. Мелочь, но именно из таких мелочей потом получается «показало на секунду меньше».

А кольцо живёт своей жизнью. Индикатор вокруг цифры — CSS-анимация из tilda-cart-1.0.min.css:

.t706__product-deleted__timer__counter__circle circle {
  stroke-dasharray: 100; stroke-dashoffset: 0; stroke-width: 2px;
  animation: t706_countdown 4s linear infinite forwards
}
@keyframes t706_countdown { 0% { stroke-dashoffset: 35 } to { stroke-dashoffset: 102 } }

Период кольца — 4 с, и цифра тоже стартует с 4 и через четыре тика исчезает, так что при нормальном ходе они совпадают. А вот infinite здесь лишний ровно до того момента, когда корзину закрывают: интервал умирает, элемент остаётся, и кольцо продолжает крутиться по кругу над замершей цифрой. Тот же расчёт в своей вёрстке даст тот же расход: одна половина отсчёта на JS, другая на CSS — и они разъезжаются при первой же паузе.

В Zero Block типа «таймер» нет, и это следствие, а не придирка

Все элементы Zero Block создаются в одной развилке, t396_artboard_build:

switch (t.getAttribute("data-elem-type")) {
  case "text":    t396_addText(r, t);    break;
  case "image":   t396_addImage(r, t);   break;
  case "shape":   t396_addShape(r, t);   break;
  case "button":  t396_addButton(r, t);  break;
  case "video":   t396_addVideo(r, t);   break;
  case "html":    t396_addHtml(r, t);    break;
  case "tooltip": t396_addTooltip(r, t); break;
  case "form":    t396_addForm(r, t, e); break;
  case "gallery": t396_addGallery(r, t, e); break;
  case "vector":  t396_addVector(r, t)
}

Ровно десять значений, и ветки default нет: неизвестный тип пропускается молча, без ошибки в консоли. Значит свой таймер в Zero Block живёт только внутри элемента html — то есть внутри врезки своего кода.

И вот что про эту врезку стоит знать заранее. t396_addHtml — единственный из десяти конструкторов, который ничего не инициализирует:

function t396_addHtml(t, e) {
  var i = t396_getEl(e);
  if (i) {
    var r = "width,height,top,left,";
    r += "container,axisx,axisy,widthunits,heightunits,leftunits,topunits";
    i.setAttribute("data-fields", r), t396_elem__renderView(i)
  }
}

Сравните с соседями: у video есть t_onFuncLoad("t396_initVideo", …), у tooltip — t396_initTooltip. У html — ни одного колбэка, ни одной подписки на пересчёт, а в списке полей только геометрия: ширина, высота, отступы, единицы. Никакого heightmode, то есть автовысоты у него нет тоже. Практический вывод: ваш таймер в Zero Block получит ровно то место, которое вы ему задали руками, и о показе таба или попапа ничего не узнает.

Часовой пояс: Тильда его измеряет и никуда не применяет

По всему разобранному рантайму getTimezoneOffset вызывается один раз, и вот где — в tilda-stat-1.0.min.js, при сборке отпечатка посетителя:

e += "cD" + (screen.colorDepth ? screen.colorDepth : "") + "|",
e += "tO" + (new Date).getTimezoneOffset() + "|",
e = c(e += "mT" + (void 0 !== navigator.maxTouchPoints ? … ) + "|")

Date.UTC — 0 вызовов. Date.parse — 0 вызовов. То есть ни одна строка рантайма не нормализует дату к поясу. Для своего таймера это значит одно: считать надо от метки времени в UTC, иначе акция «до 23:59» закончится у посетителя из Владивостока на семь часов раньше, чем вы рассчитывали, — и никто в Тильде за вас это не поправит.

Ближайший штатный аналог «своё время у каждого» — механизм «не показывать N дней» из блока T1035. И обратите внимание, где он лежит: не в общем рантайме, а в постраничном бандле tilda-blocks-page75717.min.js — том самом типе файла, в котором приезжает и код таймера:

function t1035__initDelayedVisibility(container, thumbCloseBtn) {
  var storageItem = container.getAttribute('data-storage-item');
  var delta = container.getAttribute('data-storage-delta') * 86400;
  var today = Math.floor(Date.now() / 1000);
  var lastOpen = null; var currentDelta;
  try { lastOpen = localStorage.getItem(storageItem);
        currentDelta = today - lastOpen }
  catch (e) { console.log('Your web browser does not support localStorage.') }
  if (lastOpen === null || currentDelta >= delta)
    container.classList.remove('t1035_closed')
}

Разрешение — сутки (* 86400), и всё обёрнуто в try/catch. В режиме инкогнито и при заблокированном хранилище это просто не работает, и сообщение уходит в консоль, а не пользователю.

Что часто принимают за таймер, но это не он

Анимация чисел. Её предлагают вместо таймера постоянно, а считает она вверх и про даты не знает вообще. Вот весь её ход, из t_animate__animateNumbers:

var p = 12, h = 0,
    g = setInterval(function () {
      if (u += d / 108, h = c ? … : Math.floor(u) + "", …, u >= d) {
        clearInterval(g), t.innerHTML = e; …
      }
    }, p)

Шаг 12 мс, приращение d / 108 — то есть ровно 108 шагов, примерно 1.3 секунды от нуля до значения из data-animate-number-count, независимо от самого значения. Ни дедлайна, ни направления вниз здесь нет и быть не может.

Заодно в той же функции — опечатка, которую видно только на чтении:

n = Array.prototype.filter.call(n, function (t) {
  return !t.classList.contains(".t-animate__number") })

classList.contains сравнивает с именем класса, а не с селектором, поэтому строка с точкой не совпадёт никогда, и этот фильтр не отбрасывает ничего. Последствие мягкое — стиль для восстановления берётся у первого же span, — но это ровно та ошибка, которую в своём коде допускают чаще всего.

Полезная вещь, просто не таймер, — разбор с замерами у нас в отдельной модификации.

T142. Его называют блоком таймера, а это блок-кнопка. В CSS: .t142{width:100%;padding:0 20px;display:flex;justify-content:center;box-sizing:border-box} — и на живой странице владелец дописывает к нему overflow:visible !important, чтобы выпустить тень пульсации кнопки. t143, t144, t145 — по нулю везде.

t-countdown. Класса с таким именем в Тильде нет. Поиск по подстроке даёт три вхождения, и все три сидят внутри имени атрибута: a = "yes" === o.getAttribute("data-cart-countdown"). Проверка с кавычкой, [."']t-countdown, даёт ноль.

Что из этого делать

Считайте от метки времени, а не от предыдущего значения. Раз в секунду вычисляйте разницу между дедлайном в UTC и Date.now(), а не отнимайте единицу от того, что нарисовано. Тогда таймер не потеряет секунды в фоновой вкладке и покажет одно и то же всем.

Останавливайтесь, когда вас не видно. Соглашение Тильды здесь правильное, и проще всего его повторить через IntersectionObserver плюс visibilitychange. Заодно это снимает нагрузку с телефона.

Не отдавайте CSS половину отсчёта. Если рядом с цифрой есть кольцо, полоса или заливка — двигайте их тем же кодом, который двигает цифру. Иначе получите то, что получилось у самой Тильды: кольцо крутится, цифра стоит.

Учтите цепочку появления. Таймер отодвигает кнопку больше чем на две секунды, а три кнопки в блоке выворачивают порядок. Если кнопка важна — переключите блок на ручной порядок через data-animate-order или отключите анимацию появления у записи.

Откуда числа

t_animate__assignGroupDelay выписана из tilda-animation-2.0.min.js (34.6 КБ) целиком, вместе с объявлениями всех семи выборок; арифметика в таблице пересчитана по приращениям a из этого кода. t-timer в этом файле — 1 вхождение при контрольном t-animate в 153. Отсчёт корзины выписан из tilda-cart-1.1.min.js (188.5 КБ) при контрольном t706 в 312 вхождений, правило кольца — из tilda-cart-1.0.min.css при контрольном .t706 в 190. Развилка типов элементов — из t396_artboard_build в tilda-zero-1.1.min.js (контроль t396 — 486); проверено вторым способом: определений function t396_add* в файле ровно десять, ни одного лишнего, и список совпадает со списком case. Правило .t142 — из tilda-blocks-page75717.min.css, единственное вхождение во всех 37 файлах CSS; overflow:visible !important рядом с ним снят с живой страницы autolayout-page-mo-ti.ru.html, где он выпускает тень si-button-pulse. getTimezoneOffset — 1 вызов, Date.UTC — 0, Date.parse — 0, при контрольном Date.now в 163 вызова по тем же 114 файлам.

Две ловушки замера, на которые стоит указать отдельно, потому что каждая даёт уверенный ложный вывод. t-countdown «встречается» три раза — все три внутри имени data-cart-countdown; поиск с кавычкой, [."']t-countdown, даёт ноль. Поиск msk без учёта регистра даёт 23 вхождения в семи файлах — можно решить, что Тильда где-то приводит время к московскому поясу; все 23 сидят внутри data-tilda-formskey, а ровно MSK не встречается ни разу.

И общая оговорка. Отсутствие имени в наборе файлов не равно отсутствию его в Тильде: у Тильды отложенная привязка через t_onFuncLoad (271 вхождение), и код блока может лежать в постраничном бандле, которого у нас нет. Именно это и происходит с таймером: t_timer, t_timer__init, t142_init — по нулю, и ни одно из 71 уникального имени, зарегистрированного через t_onFuncLoad, не содержит ни timer, ни countdown, ни clock. Всё написанное выше — про код, который есть, и не распространяется на код, которого нет.

Вопросы по этой статье 4

Судя по коду — да, блок есть: класс `.t-timer` зашит в ядро анимации наравне с заголовком, описанием и кнопкой, то есть ядро считает его полноправным элементом содержимого. Но кода самого блока в разобранных 114 файлах нет ни байта: он приезжает постраничным бандлом. Поэтому номер блока назвать нечем — T142, который часто называют, оказался блоком-кнопкой.