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

Табы в Тильде: разметка T817 и один return, который ломает адаптив

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

Табы в Тильде есть — это блок T817, и его разметка сделана правильно: `role="tablist"`, `aria-controls`, отдельный `<select>` для мобильных. Но табы в Тильде — это не только «показать один блок содержимого из шести»: с табом можно связать целую запись. А в той самой функции рантайма, которая решает, показывать запись или нет, стоит `return` там, где по смыслу нужен `continue`. Разберём разметку по живой странице и посчитаем, чем этот `return` выходит.

✓ ПРОВЕРЕНО 29 июля 2026 разметка блока T817 снята с живой страницы на Тильде целиком, вместе с мобильным `<select>`; посчитано, что из восьми `aria-selected` на странице ни одного со значением `true`; функция адаптивной видимости выписана из `tilda-scripts-3.0.min.js` полностью, с якорем по единственному вхождению `data-connect-with-tab` и контролем `data-screen-max` 3 из 3; перепись события `displayChanged` по 114 файлам рантайма при контрольном `addEventListener` в 1273 вхождения; оба значения `data-popup-rec-ids` в 22 страницах выписаны с тегом-носителем — рантайм Тильды из 114 файлов и 22 страницы, снятые с живых сайтов на Тильде

Задача выглядит решённой: в Тильде есть блок табов, ставим — работает. Так и есть, пока табы стоят сами по себе. Сложности начинаются на стыке: таб плюс адаптив, таб плюс Zero Block, таб плюс попап. Разберём три вещи по коду и разметке — что T817 печатает на самом деле, где у него вторая независимая разметка для мобильных, и почему одна строка в рантайме превращает «скрыть блок на телефоне» в лотерею.

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

Табы в Тильде — блок T817, разметка с полной ARIA, на узких экранах вместо полосы табов работает <select>. Zero Block умеет быть содержимым таба через data-connect-with-tab="yes". Но в функции адаптивной видимости стоит return вместо continue: одна запись, связанная с табом, отключает правила data-screen-min/data-screen-max у всех записей ниже.

Что T817 печатает на живой странице

Полоса табов — обычный список, размеченный по стандарту:

<ul class="t817__wrapper" role="tablist" data-tab-current="1">
  <li class="t817__tab" role="presentation" data-tab-number="1">
    <button type="button" class="t817__tab-name t-name t-name_xs"
            id="tab1_750744168" role="tab" aria-selected="false"
            aria-controls="content-tab1_750744168" tabindex="-1">
      Годовой доступ<br /><strong>-20%</strong>
    </button>
  </li>
  …
</ul>

Здесь всё на месте: role="tablist" на контейнере, role="presentation" на <li> — чтобы читалка не объявляла «список из шести пунктов» поверх «шести табов», role="tab" на кнопке, aria-controls на свой блок содержимого, data-tab-current на контейнере хранит номер открытого. Кнопка — настоящий <button type="button">, а не <div> с обработчиком.

И одна деталь, которую видно только если посчитать. На всей странице aria-selected встречается восемь раз, и ни одного раза со значением true. У всех кнопок tabindex="-1". То есть в том HTML, который приезжает с сервера, ни один таб не помечен выбранным и ни на один нельзя перейти табуляцией: и выбранное состояние, и участие в порядке фокуса выставляет скрипт. Пока он не отработал, полоса табов для читалки — шесть кнопок без состояния.

Мобильная полоса — это отдельный <select>

Для узких экранов T817 печатает не сжатую полосу, а вторую, независимую разметку:

<div class="t817__wrapper_mobile">
  <div class="t817__firefoxfix" style="top: 1px;bottom: 1px;right: 1px;…"></div>
  <select class="t817__select t-name" data-tab-mobile-number="1">
    <option class="t817__tab-name_mobile" value="1">
      Годовой доступ<br /><strong>-20%</strong>
    </option>
    …
  </select>
</div>

Три следствия, и все практические.

Первое: подписи табов оказываются в документе дважды — один раз в кнопках, один раз в <option>. Если вы правите подпись селектором .t817__tab-name, мобильный вариант её не получит: у него класс t817__tab-name_mobile и он лежит в другом поддереве.

Второе: внутри <option> разметка не работает. Содержимое <option> по спецификации — текст, и <br /> со <strong> там ничего не оформят. Тот самый перенос и жирная скидка, которые красиво выглядят в кнопке, в мобильном селекторе схлопнутся в одну строку. Это не баг Тильды — она честно печатает одну и ту же подпись в оба варианта, — но верстать подпись табов в расчёте на <br> не стоит.

Третье: t817__firefoxfix — заплатка под собственный внешний вид <select> в Firefox, и она позиционируется инлайновым стилем с точностью до пикселя. Своё оформление селектора будет с ней спорить.

Табы переключают не только содержимое внутри блока

У T817 есть свои блоки содержимого — на них указывает aria-controls. Но в Тильде с табом можно связать и целую запись: у записи появляется атрибут data-connect-with-tab="yes", и дальше её показ и скрытие — забота механизма табов, а не её собственных настроек.

Именно здесь появляется тонкое место, потому что за видимость записей в Тильде отвечает ещё один, совсем отдельный механизм — настройки «показывать только на экранах шире/уже». Они живут в атрибутах data-screen-min и data-screen-max на записи, и раздаёт их вот эта функция из tilda-scripts-3.0.min.js:

function e() {
  var t = window.isMobile
        ? document.documentElement.clientWidth
        : window.innerWidth,
      e = document.querySelectorAll(".r[data-screen-max], .r[data-screen-min]"),
      n, o, i;
  -1 !== navigator.userAgent.indexOf("Instagram") && (t = screen.width);
  for (var r = 0; r < e.length; r++) {
    var a = e[r], l = a.id.replace("rec", "");
    if ("yes" === a.getAttribute("data-connect-with-tab") ||
        document.querySelector('[data-popup-rec-ids="' + l + '"]')) return;
    i = getComputedStyle(a).display,
    (n = a.getAttribute("data-screen-max")) || (n = 1e4),
    (o = a.getAttribute("data-screen-min")) || (o = 0),
    n = parseInt(n),
    (o = parseInt(o)) <= n && (t <= n && t > o
      ? "block" !== i && (a.style.display = "block")
      : "none" !== i && (a.style.display = "none"))
  }
}

Находка: return вместо continue

Смотрите на условие внутри цикла. Оно правильное по смыслу: если запись отдана под управление табам или лежит в попапе, трогать её display нельзя — иначе адаптив и табы будут перебивать друг друга. Но выход из этой ситуации выбран return, а не continue.

continue пропустил бы одну запись и пошёл дальше. return выходит из функции целиком. Все записи, которые в разметке стоят ниже найденной, до своей проверки не доходят: они остаются с той видимостью, которую им дал CSS и предыдущий прогон.

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

Функция вызывается один раз на t_onReady и потом на каждый resize, придушенный до одного вызова в 200 мс:

t_onReady((function () {
  t(), e(),
  window.addEventListener("resize", t_throttle((function () { t() }), 200)),
  window.addEventListener("resize", t_throttle((function () { e() }), 200))
}))

То есть промах повторяется при каждом изменении размера окна, а не только при загрузке.

Честная оговорка о границах этого вывода: в 22 разобранных страницах атрибут data-connect-with-tab не встретился ни разу, а записей с data-screen-min/data-screen-max нашлось 23 — 11 на одной странице и 12 на другой. То есть страницы, на которой этот return можно было бы застать за работой, у нас нет. Утверждение прочитано из кода, а не снято с живого сайта.

Три подробности той же функции, которые пригодятся

Границы несимметричны. Условие показа — t <= n && t > o, где n — data-screen-max, o — data-screen-min. Максимум включающий, минимум исключающий: при data-screen-max="640" запись на ширине ровно 640 видна, при data-screen-min="640" на ширине ровно 640 — уже нет. Значения по умолчанию: максимум 10000, минимум 0.

Минимум больше максимума — правило просто не применяется. Проверка o <= n стоит перед всей развилкой. Если минимум задан больше максимума, запись не будет ни показана, ни скрыта: она останется как есть, без ошибки и без следа в консоли.

Внутри Instagram ширина берётся не окна, а экрана: -1 !== navigator.userAgent.indexOf("Instagram") && (t = screen.width). Во встроенном браузере Instagram решение о видимости принимается по физической ширине экрана. А если Тильда считает посетителя мобильным, ширина берётся из documentElement.clientWidth, а не из window.innerWidth — то есть без полосы прокрутки. На пограничных значениях это разница в 15–17 пикселей.

Второе условие того же if — попапы, и два написания атрибута

Вторая половина условия ищет попап, который показывает эту запись:

document.querySelector('[data-popup-rec-ids="' + l + '"]')

где l = a.id.replace("rec", "") — идентификатор записи без приставки rec. А в разметке этот атрибут встречается в двух написаниях. На одной и той же странице, на двух соседних попапах:

<div class="t-popup " data-anim="fadein" data-anim-timeout="0.3"
     data-tooltip-hook="#wheel-fortune" data-popup-rec-ids="750744165"
     role="dialog" aria-modal="true" tabindex="-1">

<div class="t-popup " data-anim="fadein" data-anim-timeout="0.3"
     data-tooltip-hook="#popup:m-tariffs" data-popup-rec-ids="rec2148318711"
     role="dialog" aria-modal="true" tabindex="-1">

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

Zero Block внутри таба: единственный штатный мост

Zero Block считает позиции элементов по факту размеров контейнера. Пока таб закрыт, размеров нет, и после открытия элементы разъезжаются. Для этого в Тильде есть мост — в tilda-zero-1.1.min.js:

e && i && r && "yes" === e.getAttribute("data-connect-with-tab") &&
  i.addEventListener("displayChanged", function () {
    t396_allelems__renderView(r);
    t396_allgroups__renderView(r);
    t396_doResize(t, !0);
  });

Три вызова подряд: заново отрисовать все элементы, заново отрисовать все группы, и полный пересчёт с принудительным флагом. Это ровно то, что нужно, и это работает без вашего участия — при одном условии: у записи должен стоять data-connect-with-tab="yes".

Обратная сторона у моста тоже есть. t396_doResize стирает инлайновые стили с .tn-atom — обнуляет transformOrigin, fontSize, webkitTextSizeAdjust. Если вы дописывали Zero Block свои инлайновые стили скриптом, открытие таба их сотрёт.

Кто ещё живёт на displayChanged

Событие displayChanged — общая шина «меня показали, пересчитайся». По 114 файлам рантайма оно встречается 51 раз. Разложим на слушателей и рассыльщиков (контроль замера: addEventListener в тех же файлах — 1273 вхождения, то есть обход файлов рабочий).

файлслушаетрассылает
tilda-slds-1.4.min.js — слайдер4—
tilda-zero-gallery-1.0.min.js4—
tilda-zero-1.1.min.js2—
tilda-blocks-page75717.min.js — постраничный2—
tilda-catalog-1.1.min.js — каталог1—
tilda-menu-1.0.min.js1—
tilda-menu-1.1.min.js1—
tilda-cover-1.0.min.js — обложка1—
tilda-animation-sbs-1.0.min.js1—
tilda-autoscale-1.0.min.js—2
tilda-forms-1.0.min.js—1

Читается это так: подписчиков девять файлов, рассылают событие всего два. Всё, что Тильда умеет пересчитывать после показа, — слайдер, галерея Zero Block, меню, обложка, каталог, сама разметка Zero Block. Всё, чего в списке нет, пересчитывать придётся самому: своя карта, свой график, свой редактор — они об открытии таба не узнают.

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

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

Порядок блоков — часть настройки. Блок с табами и попапы лучше держать ниже записей, у которых настроена видимость по ширине экрана. Это не про красоту, это единственный способ обойти return без своего кода.

Zero Block внутри таба связывайте штатно. Мост через data-connect-with-tab делает всё, что надо, и делает это правильнее любого самодельного setTimeout после открытия.

Свои виджеты внутри таба обновляйте по displayChanged. Событие уже рассылается, слушателя достаточно повесить на запись. Собственный таймер на «наверное, уже показали» тут проигрывает всегда.

Откуда числа

Разметка T817 снята с сохранённой страницы живого сайта на Тильде, <ul role="tablist"> и <select class="t817__select"> выписаны из неё целиком. aria-selected — 8 вхождений на странице, из них со значением true — 0, data-tab-current — 1. Функция адаптивной видимости выписана из tilda-scripts-3.0.min.js (22.8 КБ) полностью; чтобы не попасть в соседнюю функцию, якорем взято единственное вхождение data-connect-with-tab, а контроль data-screen-max дал ожидаемые 3 из 3. Перепись displayChanged — 51 вхождение по 114 файлам, при контрольном addEventListener в 1273 вхождения. Оба значения data-popup-rec-ids найдены в 22 страницах и выписаны вместе с тегом-носителем; контроль data-record-type по тем же страницам — 219 вхождений.

И общая оговорка, которая относится ко всем разборам рантайма Тильды. Отсутствие имени в наборе файлов не равно отсутствию его в Тильде: у Тильды отложенная привязка через t_onFuncLoad, и функция может вызываться в файле, который у нас есть, а определяться в постраничном бандле, которого нет. Проверка на этот счёт: t817_init в 114 файлах не встречается ни разу — то есть сам инициализатор табов лежит именно в таком бандле. Всё, что написано выше, читается из разметки и из кода, которые есть, и не распространяется на код, которого нет.

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

T817. На живой странице он печатает `<ul class="t817__wrapper" role="tablist" data-tab-current="1">`, внутри — `<li role="presentation">` с кнопкой `role="tab"`, у кнопки `aria-controls` на свой блок содержимого. Для узких экранов рядом лежит вторая, независимая разметка: `<select class="t817__select">` с теми же подписями в `<option>`.