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

Плавное появление при скролле: порог наблюдателя и высота секции

28 июля 2026 7 МИН КОМАНДА MODKOD

Порог IntersectionObserver считается от площади самого элемента. У артборда Zero Block высотой в шесть экранов видимая доля не дойдёт до 15 процентов ни при каком скролле, и наблюдатель не сработает никогда — текст останется спрятанным навсегда. Вторая причина другая: на момент подписки артборда ещё нет, его высота — ноль. Разбираем оба случая и правило, из которого следует, что прятать элемент правилом в CSS нельзя.

✓ ПРОВЕРЕНО 24 июля 2026 порог и нижний rootMargin наблюдателя, страховка по координатам, серия повторных проходов после готовности документа и наблюдение за — стенд с сохранённой страницей Тильды и её рантаймом, Chromium

Симптом узнаётся сразу: секция в экране, текст в разметке есть, в консоли чисто, а на экране пусто или элементы стоят в конечном положении без всякой анимации. У этого две разные причины, и лечатся они по-разному. Первая — в том, как считается порог наблюдателя. Вторая — в том, что артборда Zero Block на момент подписки ещё не существует.

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

Если контент спрятан и плавное появление не срабатывает — виноват порог: IntersectionObserver считает долю наблюдаемого элемента, а не экрана: у секции в шесть экранов пятнадцать процентов не наберутся никогда. Нужен порог 0 и отрицательный rootMargin. Если появляется, но без анимации — виноват момент подписки: артборд Zero Block собирается после готовности документа. Правило: прятать элемент правилом в CSS нельзя, начальное состояние ставит скрипт.

Почему порог по доле площади — гарантированный провал

Порог — это доля площади наблюдаемого элемента, попавшая в экран. Для элемента выше экрана максимум этой доли посчитать легко: он равен высоте экрана, делённой на высоту элемента. Ни при каком скролле больше не станет — элемент просто не влезает.

Экран ноутбука — 800 пикселей. Тогда для порога 0.15:

Высота артбордаМаксимум видимой долиПорог 0.15
1600 px0.50сработает
2400 px0.33сработает, когда войдут 360 px
4000 px0.20сработает на самом пределе
6000 px0.13не сработает никогда
9000 px0.09не сработает никогда

Артборд Zero Block на шесть тысяч пикселей — это не экзотика, а обычная главная страница с несколькими экранами внутри одного блока. А на телефоне доля падает с двух сторон: экран ниже, мобильный артборд выше. Поэтому такие поломки почти всегда приносят с телефона.

Настройка, которая работает независимо от высоты:

new IntersectionObserver(handler, {
  threshold: 0,
  rootMargin: '0px 0px -8% 0px',
});

threshold: 0 — сработать, как только пересечение появилось. Отрицательный нижний rootMargin подрезает низ области наблюдения на 8 процентов высоты экрана: секция обязана войти в экран немного, а не коснуться его края. Это то же самое, зачем ставят порог, но считается от экрана, а не от элемента, — и поэтому работает на артборде любой высоты.

Почему одного наблюдателя мало

Наблюдатель может исправно работать и при этом не сообщить о пересечении ни разу. Два случая встречаются регулярно:

  • Секция внутри контейнера с overflow. Наблюдатель по умолчанию сравнивает элемент с областью просмотра и обрезку предками не учитывает. Отчёт приходит, но не тот, который нужен.
  • Нулевая высота на момент подписки. Артборд, размеры которого Zero Block ещё не выставил, занимает ноль пикселей. Подписываться на такой элемент можно, толку от подписки нет.

Отсюда — вторая, независимая проверка. Не «на всякий случай», а потому что у первой есть известные слепые зоны.

Страховка по координатам как пассивный слушатель

Вторая проверка спрашивает геометрию напрямую:

function visible(el) {
  var r = el.getBoundingClientRect();
  var h = window.innerHeight || document.documentElement.clientHeight;
  // Нулевой размер — это «ещё не разложено», а не «видно».
  if (!r.height && !r.width) return false;
  return r.top < h * 0.92 && r.bottom > 0;
}

Здесь важны три вещи. Проверка нулевого размера отсекает не разложенный артборд: без неё эффект запустился бы на элементе, которого ещё нет на экране, и посетитель увидел бы конец анимации вместо начала. Коэффициент 0.92 — те же 8 процентов, что в rootMargin наблюдателя: оба пути должны срабатывать в одном месте, иначе поведение зависит от того, какой из них выиграл гонку. А r.bottom > 0 означает, что секция ещё не ушла вверх.

Вешать это надо пассивным слушателем и снимать после первого срабатывания:

window.addEventListener('scroll', onMove, { passive: true });
window.addEventListener('resize', onMove, { passive: true });

passive: true — обещание браузеру, что слушатель не отменит скролл; без него браузер ждёт вашего кода, прежде чем прокрутить страницу. Пока никто не скроллит, проверка не стоит ничего. После первого срабатывания и наблюдатель, и слушатели снимаются, а флаг «уже сработало» не даёт второму пути сделать работу заново.

Артборд собирается после готовности документа

Второй класс поломок — про время, а не про видимость. Артборд Zero Block существует не с момента загрузки HTML: его собирает скрипт Тильды, и происходит это после DOMContentLoaded. Ленивые блоки приезжают ещё позже. Код, который один раз прошёл по странице и закончил, застаёт пустоту и уходит.

Поэтому первый проход дополняется короткой серией повторов:

ready(function () {
  watch();
  start(id, params);
  var tries = [150, 400, 1000, 2500];
  for (var i = 0; i < tries.length; i++) setTimeout(schedule, tries[i]);
});

Четыре повтора укладываются в две с половиной секунды и стоят почти ничего: обработанные секции помечены и пропускаются, а сам пересмотр страницы отложен на 80 миллисекунд, чтобы серия срабатываний слилась в один проход. Одного таймаута здесь мало не потому, что 150 миллисекунд — плохое число, а потому, что верного числа не существует: он зависит от сети, устройства и количества блоков.

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

Наблюдать надо #allrecords, а не document

Новые секции появляются в DOM и после первого прохода. Ловить их нужно наблюдателем за мутациями, и здесь два решения, которые легко сделать неправильно.

Корень — контейнер записей Тильды. #allrecords — наименьший стабильный узел, внутри которого появляются и пересобираются секции:

var root = document.getElementById('allrecords') || document.body;
new MutationObserver(schedule).observe(root, { childList: true, subtree: true });

Следить только за childList. Соблазн добавить attributes: true понятен — Zero Block правит инлайновые стили при перерисовке. Но собственные атрибуты скрипта тогда будят наблюдателя, наблюдатель зовёт проход, проход ставит атрибуты. Метка на секции спасёт от повторной работы, но не от бесконечного круга вызовов.

Метка на секции — до запуска, а не после

Способ не сделать работу дважды — атрибут на секции. Важно, в какой момент он ставится:

if (el.hasAttribute(attr)) continue;
// Метку ставим до запуска: если мод упадёт, второй раз его не зовём.
el.setAttribute(attr, '1');
try {
  mod.setup(el, params);
} catch (e) {
  warn(id, e);
}

Если ставить метку после setup, то упавший код останется без метки — и его позовут снова на каждом из четырёх повторов и на каждой мутации. Одна ошибка превратится в десять, а сообщение в консоли — в поток, в котором не видно первой причины.

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

Правило, из которого всё это следует

Контент не остаётся невидимым ни при каком исходе. Из этого правила выводятся все решения выше, и оно же запрещает самый распространённый способ прятать элементы:

/* Так делать нельзя. */
.mk-reveal .tn-atom {
  opacity: 0;
}

Правило в CSS работает всегда — в том числе когда скрипта нет. Блок T123 не опубликован, сеть не отдала файл, в другом сниппете на странице синтаксическая ошибка — и текст спрятан навсегда, без единой записи в консоли. Хуже этого на странице не случается ничего.

Поэтому начальное состояние ставит скрипт, и ставит его инлайном: у Zero Block правило по классу секции всё равно проигрывает — почему CSS по классу проигрывает правилу Zero Block разобрано отдельно. Скрипт не доехал — текст просто виден. В CSS остаётся только то, от чего нет вреда: will-change и сброс анимации для тех, кто попросил меньше движения.

Что проверить у себя за пять минут

  1. Высота секции и порог. Разделите высоту экрана на высоту секции. Если получилось меньше вашего порога — вы нашли причину.
  2. Порог 0 плюс отрицательный нижний rootMargin вместо доли площади.
  3. Проверка на нулевой размер перед тем, как считать элемент видимым.
  4. Повторные проходы после готовности документа и обработчик ресайза: артборд собирается позже и пересобирается потом.
  5. Наблюдатель за мутациями смотрит на #allrecords и только на childList.
  6. Метка на секции ставится до запуска, и стоит она на секции, а не на элементе.
  7. В CSS нет opacity: 0 ни у одного элемента, который должен появиться. Всё начальное состояние — из скрипта.

Оба мода, на которых это проверено, работают на эффектах текста в каталоге: проявление строк по скроллу и счётчик чисел. Внутри у них одно и то же ядро — то, код которого приведён выше.

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

Максимальная видимая доля элемента выше экрана равна высоте экрана, делённой на высоту элемента. На телефоне экран ниже, а мобильный артборд обычно выше — доля падает с двух сторон сразу и перестаёт дотягиваться до порога. Ровно поэтому такие поломки почти всегда находят на телефоне, а на мониторе не воспроизводят.