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

Ниже 1200 пикселей Тильда снимает анимацию сама: четыре выключателя

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

Порог один во всех четырёх файлах анимации — 1200 пикселей, — а выключателей за ним четыре, и работают они по-разному: один снимает класс `t-animate`, второй возвращает `null` вместо настроек, третий глушит переходы, четвёртый показывает элемент сразу. Замерено: у элемента Zero Block анимация ниже порога включается атрибутом, у обычного блока — никогда, а одна группа в записи гасит анимацию всей записи вместе с соседями.

✓ ПРОВЕРЕНО 29 июля 2026 снятие класса t-animate настоящим tilda-animation-2.0 на восьми видах разметки при 1280 и 1000 пикселях, граница порога с точностью до пикселя и создание span счётчика чисел — Chromium, файл анимации из рантайма Тильды без правок

Симптом описывают почти всегда одинаково. На компьютере всё появляется как задумано; открываешь тот же адрес с телефона — блоки просто стоят на месте. Ничего не мигает, в консоли пусто, разметка та же. Так и должно быть: ниже 1200 пикселей рантайм Тильды выключает анимацию сам, и делает это в четырёх местах независимо друг от друга.

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

Порог — 1200 пикселей ширины окна. Ниже него Тильда снимает у элементов класс t-animate, а покадровой анимации не строит вовсе. Отменяется это единственным атрибутом — data-animate-mobile="y" на обёртке элемента Zero Block. У обычного блока такой обёртки нет, поэтому там анимация ниже порога выключена безусловно.

Один порог, четыре выключателя

Файлов анимации в рантайме четыре, и в каждом порог свой, отдельно написанный. Общее у них только число.

файлфункциячто делает ниже 1200
tilda-animation-2.0t_animate__checkMobileснимает класс t-animate
tilda-animation-2.0t_animate__getAttrByResBaseвозвращает null вместо любой настройки, ставит transition: none
tilda-animation-extt_animationExt__getAttrByResто же для параллакса и «прилипания»
tilda-animation-sbst_animationSBS__getAnimOptionsне строит шаги и показывает элемент сразу

Разница между первым и вторым важна на практике. Первый убирает саму анимацию. Второй оставляет её включённой, но обесточивает: style, distance, scale, delay, duration, trigger-offset — все шесть настроек приходят пустыми, и переходы вдобавок глушатся прямо в inline-стиле.

Что именно снимается

Точка сбора — t_animate__startAnimation. Она набирает три списка:

var t = document.querySelectorAll('.r'),
    e = Array.prototype.filter.call(t, function (t) {
      return t.querySelector('.t-animate[data-animate-group=yes]');
    }),
    a = Array.prototype.filter.call(t, function (t) {
      return t.querySelector('.t-animate[data-animate-chain="yes"]');
    }),
    n = document.querySelectorAll('.t-animate');

n — все анимированные элементы страницы, без разбора блоков: селектор .t-animate берётся от document. e и a — не элементы, а записи (.r, обёртка блока Тильды), внутри которых нашлась групповая или цепочечная анимация. Это различие и даёт самое неожиданное следствие, до него дойдём.

Дальше — сам порог, одной строкой:

window.innerWidth < 1200 &&
  (e = t_animate__checkMobile(e), a = t_animate__checkMobile(a), n = t_animate__checkMobile(n));

И фильтр, который решает судьбу каждого:

function t_animate__checkMobile(t) {
  return t.filter(function (t) {
    var e = t.closest('.t396__elem, .t396__group');
    return (
      !(!e || 'y' !== e.getAttribute('data-animate-mobile')) ||
      (t.classList.contains('r')
        ? Array.prototype.forEach.call(t.querySelectorAll('.t-animate'), function (t) {
            t_animate__removeAnimationClass(t, '');
          })
        : t_animate__removeAnimationClass(t, ''),
      false)
    );
  });
}

Читается так: элемент остаётся в списке, только если у него есть предок .t396__elem или .t396__group и у этого предка data-animate-mobile равен y. Иначе вызывается t_animate__removeAnimationClass, а он делает ровно одно — classList.remove('t-animate').

Отсюда первый твёрдый вывод. Предки .t396__elem и .t396__group существуют только внутри Zero Block. У элемента обычного блока — T142, T004, любого — такого предка нет, closest возвращает null, первое условие ложно, класс снимается. Никакая настройка это не меняет, потому что атрибут читается не с элемента, а с обёртки, которой в обычном блоке не бывает.

Замер: восемь видов разметки

Взят настоящий tilda-animation-2.0.min.js из рантайма, без правок, и восемь вариантов разметки. Вызов t_animate__init(), пауза 1800 мс (сама Тильда стартует анимацию через 1500), затем проверка класса.

разметкапри 1280при 1000
обычный блок, одиночный элементестьснят
обычный блок, группаестьснят
обычный блок, цепочкаестьснят
Zero Block без data-animate-mobileестьснят
Zero Block с data-animate-mobile="y"естьесть
Zero Block, группа с data-animate-mobile="y"естьснят
та же запись: группа с атрибутоместьснят
та же запись: одиночный с атрибутоместьснят

Пятая строка — единственная, где анимация ниже порога выживает. Всё остальное гаснет.

Границу проверили отдельно, по пикселю: при ширине 1198 и 1199 класса нет и <span class="t-animate__number"> не создан, при 1200 и 1201 есть и то, и другое. Сравнения в коде строгие — < 1200 для класса и >= 1200 для чисел, — так что ровно на 1200 работает всё.

Правило, которого в коде не видно

Шестая, седьмая и восьмая строки таблицы — про одно и то же, и это самая дорогая находка замера.

Группа в Zero Block с выставленным атрибутом всё равно гаснет. Причина в том, что в список e попала не сама группа, а запись, в которой она лежит. У записи .r предка .t396__elem нет по определению — она сама верхний уровень блока. Значит первое условие фильтра ложно, а поскольку у неё есть класс r, работает не одиночное снятие, а обход: t.querySelectorAll('.t-animate') и снятие класса у каждого найденного элемента внутри записи.

Последняя строка таблицы это подтверждает прямо. В одной записи стоят два элемента: группа с атрибутом и одиночный элемент, у которого атрибут выставлен на своём собственном .t396__elem. Тот же самый одиночный элемент в отдельной записи ниже порога выживает (строка пять). Здесь — гибнет, потому что запись обошли целиком.

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

Числа: свой порог, своя причина

У счётчика чисел порог отдельный и написан в другой функции — t_animate__init:

document.documentElement.clientWidth >= 1200 && t_animate__parseNumberText();

t_animate__parseNumberText — единственное место во всём файле, где создаётся <span class="t-animate__number">. Не вызвали её — span не появился, а таймер t_animate__animateNumbers пишет результат так:

f && (f.textContent = h);

f — тот самый span, найденный через querySelector. Его нет, значит f равен null, условие ложно, и таймер честно отсчитывает свои 108 шагов по 12 мс в никуда. Число на странице просто стоит.

Обратите внимание на две разные мерки в одном файле: класс снимается по window.innerWidth, а числа разбираются по document.documentElement.clientWidth. Первое включает полосу прокрутки, второе — нет, так что при полосе шириной 15 пикселей пороги расходятся. В нашем замере они совпали ровно: у Chromium в headless полоса накладная и ширины не занимает, innerWidth и clientWidth равны на всех семи проверенных ширинах. То есть расхождение существует только там, где полоса отнимает место у страницы, и его величина — это её ширина.

Скорость счётчика зашита теми же 108 шагами по 12 мс, то есть 1296 мс, и не настраивается. У мода «Анимация чисел» длительность — параметр, а порога ширины в нём нет ни одного; ниже 1200 он считает так же, как выше.

Ещё два порога, о которых не предупреждают

Ниже 1200 выключается не только появление.

Мышиный параллакс (t_animateParallax__moveEl в tilda-animation-ext) не инициализируется совсем, если выполняются оба условия — 'ontouchend' in document и clientWidth < 1200. Тело функции целиком лежит внутри проверки, так что на планшете в портретной ориентации мышиного параллакса нет по устройству, а не из-за отсутствия мыши.

Порог 980 — отдельный, и он не про выключение, а про побочный эффект. t_animate__preventHorizontalScroll собирает элементы с data-animate-style="fadeinleft", у которых правый край уже выходит за window.innerWidth, и вешает их записи класс t-animate__overflow-x-hidden — то есть чинит горизонтальную полосу прокрутки, которую сам же эффект и создаёт. Ниже 980 не вешает: !e.length || window.innerWidth < 980 || e.forEach(…). Полоса остаётся — та же, о которой речь в разборе горизонтального скролла.

В этом же отборе стоит исключение, которое стоит знать отдельно от порога:

return !t.closest('.t396__artboard, .t-cover') && t.getBoundingClientRect().right > window.innerWidth;

Элементы внутри артборда Zero Block и внутри обложки в отбор не попадают вообще — значит защиты от горизонтальной полосы у них нет ни на какой ширине, а не только ниже 980.

Там же ниже 980 работает t_animate__removeInlineAnimStyles: на каждое изменение размера окна он сбрасывает inline transform, transitionDuration и transitionDelay у .t396__elem.t-animate:not(.t-animate_wait).

«Мобильный» — три разных определения

Слово одно, проверок три, и они не совпадают:

// tilda-animation-ext
window.t_animationExt__isMobile =
  /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) ||
  'ontouchend' in document;

// tilda-zero-1.1
window.t396__isMobile =
  /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) ||
  navigator.userAgent.indexOf('Instagram') > -1;

Списки устройств одинаковые, хвосты — нет. У ext в «мобильные» попадает всё, где есть касания. У Zero Block касания не учитываются вовсе, зато учитывается встроенный браузер Instagram. Ноутбук с сенсорным экраном для одного файла мобильный, для другого — нет, и это не опечатка в статье, а два разных условия в двух файлах, которые работают на одной странице.

Третье определение живёт в покадровой анимации: animationSBS__isIOS считает своим ещё и Macintosh с касаниями — то есть iPad, который представляется компьютером. У него же триггер hover подменяется на click: наведения на касании нет, и вместо него подписывается нажатие.

Что включается, а что нет

Коротко и без обхода:

  1. Элемент Zero Block — включается атрибутом data-animate-mobile="y" на .t396__elem. Замерено, работает.
  2. Группа или цепочка в Zero Block — не включается. Проверка идёт по записи, а у записи атрибута быть не может.
  3. Любой элемент в записи, где есть группа или цепочка — не включается, даже со своим атрибутом. Гибнет вся запись.
  4. Обычный блок — не включается никогда. Атрибут ищут на обёртке Zero Block, которой там нет.
  5. Счётчик чисел — не включается ничем: порог стоит до создания span, и атрибут в этой ветке не участвует.
  6. Мышиный параллакс на касании — не включается: условие не про настройку.

Что остаётся в случаях 2–6 — своя анимация в блоке с кодом. Она не проходит ни через один из четырёх выключателей, потому что не пользуется классом t-animate и не спрашивает data-animate-mobile: браузер применяет её на любой ширине. Именно так сделаны проявление текста по скроллу и анимация чисел — ни в одном из пяти собранных модов нет ни одного обращения к innerWidth или clientWidth.

Как проверить у себя

Три строки в консоли на опубликованной странице, с телефонной шириной окна:

// 1. Сколько анимированных элементов осталось живыми
document.querySelectorAll('.t-animate').length;

// 2. У каких обёрток выставлен атрибут
[...document.querySelectorAll('[data-animate-mobile]')].map((el) => el.className);

// 3. Есть ли в записи групповая анимация — из-за неё гаснет вся запись
[...document.querySelectorAll('.r')].filter((r) =>
  r.querySelector('.t-animate[data-animate-group=yes], .t-animate[data-animate-chain="yes"]')
).length;

Первый ноль при живой разметке означает, что сработал checkMobile. Если во втором пусто — атрибут до страницы не доехал. Если в третьем не ноль, а во втором что-то есть, вы попали в правило про запись целиком.

Отдельная проверка, которая экономит время: анимация могла не сработать и вовсе не по ширине. Так бывает, когда своё правило проигрывает по специфичности собственному правилу Zero Block — этот случай разобран в анимации в зеро блоке, и по симптому он неотличим от порога. Разводит их вторая строка: при пороге класс t-animate снят, при специфичности — на месте.

Чего в этих файлах нет вообще

Системную настройку «уменьшить движение» ни один из четырёх файлов не читает: prefers-reduced-motion и matchMedia в них не встречаются ни разу. Порог по ширине есть, порога по просьбе пользователя — нет. Что это значит для доступности и как это закрывают своим кодом — в разборе доступности анимации.

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

Так устроен выход из покадровой анимации: вместе с отказом считать шаги Тильда снимает с элемента класс t396__elem--anim-hidden, то есть ту самую прозрачность, которая держала элемент скрытым до начала анимации. Элемент сразу оказывается в конечном состоянии. Это не сбой, а нарочный запасной путь — иначе на телефоне остались бы пустые места.