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

Почему в заявках с Тильды недозвонимые номера

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

Родная проверка телефона в Тильде смотрит на то, из каких символов состоит номер, и на явный мусор вида 000 и 111. Длину она не смотрит вообще: «+7 999» проходит валидацию и уезжает в заявку как готовый контакт. Закрыть эту дыру можно, но не везде и не так, как обычно советуют: у половины полей телефона в Тильде есть собственный хозяин.

✓ ПРОВЕРЕНО 24 июля 2026 правило phone в рантайме форм, пересборка поля автоматической маской и потеря aria-describedby, разбор номера и место курсора набор за набором — стенд с формой Тильды и её работающим рантаймом, Chromium; разбор номера — прогоном без браузера

Заявка пришла, телефон в ней есть, менеджер звонит — номер не существует. В поле стояло «+7 999», посетитель отвлёкся, форма его приняла. Разбираемся, что именно проверяет Тильда, что она проверять не умеет и почему готовый совет «навесь свою маску» ломает больше, чем исправляет.

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

Правило phone проверяет, из каких символов состоит номер, и не проверяет, сколько их. Дыра настоящая, закрывается тремя способами:

  1. Включить в редакторе маску телефона: она задаёт и набор символов, и их количество.
  2. Если маска не подходит — своё сообщение о неполном номере, и только в родной контейнер ошибки.
  3. Отправку не запрещать: её делает клик по кнопке, а не событие формы.

Что проверяет правило phone

Валидация формы обходит поля с классом js-tilda-rule и разбирает значение data-tilda-rule. В ветке телефона проверок ровно две:

  • набор символов — ^[0-9()+-]+$, то есть цифры, скобки, плюс и дефис. Набор расширяется, если на поле стоит data-tilda-mask;
  • мусорные последовательности — 000, 111, 222, 333, 444, 5555, 666, 8888, начиная с первой позиции.

И всё. Длины в этой ветке нет ни в каком виде — ни минимальной, ни по шаблону страны. Поэтому проходят:

Значение в полеПравило phoneДозвонимость
+7 999пропускаетнет
+7 (999) 12пропускаетнет
7пропускаетнет
+7 999 111 11 11не пропускает — мусор 111да, если номер настоящий

Последняя строка — отдельная неприятность: правило отсекает не только фальшивку, но и настоящие номера с повторами. Это не то, что стоит трогать своим кодом, но знать об этом полезно, когда клиент говорит «форма не принимает мой телефон».

Три вида поля телефона, и у двух есть хозяин

Прежде чем писать хоть строку, нужно понять, с каким полем вы имеете дело. Их три, и они ведут себя по-разному:

Вид поляКак узнатьКто владеет
Автоматическая маскакласс js-phonemask-input, атрибут data-phonemask-maskcountry; страница грузит tilda-phone-mask-1.1.min.jsТильда
Маска по шаблону дизайнеракласс js-tilda-mask вместе с data-tilda-maskТильда
Без маскиtype="tel" и data-tilda-rule="phone", масок нетсвободно

Своя проверка имеет смысл только в третьей строке. В первых двух формат уже кто-то дописывает по ходу набора, и второй такой же обработчик — это не «улучшение», а конфликт.

Почему на поле с маской свою вешать нельзя

Маска работает на событии input: посетитель нажал цифру — маска перечитала значение, вставила скобки и вернула курсор на место. Ваш обработчик делает то же самое. Дальше два обработчика по очереди переписывают value и по очереди двигают курсор, и порядок между ними не определён ничем.

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

Поэтому правильное поведение — не «аккуратно вписаться», а не трогать вовсе. Поле с маской помечается как чужое и пропускается.

Маска подменяет поле целиком, и уносит aria-describedby

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

До запуска маски в разметке стоит примерно это:

<input class="t-input js-phonemask-input js-tilda-rule"
       aria-describedby="error_1234567890">

После запуска на этом месте другое дерево. Появляется обёртка .t-input-phonemask__wrap с выбором страны, поле для набора становится вот таким:

<input type="tel" class="t-input t-input-phonemask"
       data-phonemask-iso="ru" data-phonemask-code="+7"
       data-phonemask-mask="+7(000) 000-00-00"
       data-phonemask-without-code="(000) 000-00-00" maxlength="15">

Последствий три, и каждое ломает наивный код:

  • Код страны в видимое поле не входит. Он в выпадающем списке, а номер с кодом маска кладёт в скрытое js-phonemask-result — именно оно уходит на сервер. Читая видимое поле, вы читаете номер без кода страны.
  • aria-describedby на пересобранном поле теряется. Связка поля с контейнером ошибки, которую Тильда написала в разметке, после запуска маски пропадает. Это её собственная потеря, но помнить о ней придётся вам.
  • Класса js-phonemask-input на новом поле нет. Метка, поставленная до запуска маски, ушла вместе со старым узлом — вместе с самим узлом.

Как искать маску, чтобы не промахнуться

Из третьего пункта следует правило: искать не по одному признаку, а по всем следам сразу, и делать это на каждом событии, а не один раз при запуске.

if (
  cls.indexOf(' js-phonemask-input ') !== -1 ||
  cls.indexOf(' js-phonemask-result ') !== -1 ||
  cls.indexOf(' t-input-phonemask ') !== -1 ||
  cls.indexOf(' js-tilda-mask ') !== -1 ||
  el.getAttribute('data-tilda-mask') ||
  hasPhonemaskAttr(el) ||          // любой data-phonemask-*
  closest(el, 't-input-phonemask__wrap')
) {
  return 'tilda';                  // поле чужое, не трогаем
}

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

// Zero Block собирает форму скриптом уже после загрузки, а всплывающая форма
// живёт в разметке всегда. Один обработчик на секцию покрывает и то, и другое.
section.addEventListener('input', function (e) { … });

// focusout, а не blur: blur не всплывает, до секции он бы не дошёл.
section.addEventListener('focusout', function (e) { … });

Подписка на конкретные поля здесь не работает по построению: в Zero Block полей на момент запуска ещё нет.

Своё сообщение — в родной контейнер ошибки

Под каждым полем Тильды уже лежит контейнер .t-input-error внутри .t-input-group. Он размечен дизайнером, уже имеет aria-live="polite" и уже связан с полем через aria-describedby. Своя плашка рядом с ним — это второе сообщение об ошибке в одном поле и разорванная связка с программой чтения экрана.

Пишем туда же, куда пишет сама Тильда:

function showError(el, text) {
  var group = closest(el, 't-input-group');
  if (!group) return;
  var boxes = group.querySelectorAll('.t-input-error');
  if (!boxes.length) return;

  addClass(group, 'js-error-control-box');
  for (var i = 0; i < boxes.length; i++) boxes[i].textContent = text;
  // Свою надпись помечаем, чтобы потом стереть только её.
  group.setAttribute('data-mk-phone', 'error');
}

Класс js-error-control-box на группе — то же, что вешает родной показ ошибок, поэтому вид сообщения останется тем, который настроил дизайнер.

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

function clearError(el) {
  var group = closest(el, 't-input-group');
  if (!group || group.getAttribute('data-mk-phone') !== 'error') return;
  …
}

И момент показа: говорить о неполном номере — на уходе из поля, а снимать надпись — сразу, как только номер добран. Ждать ухода, чтобы убрать сообщение, незачем: посетитель уже всё исправил, а надпись под полем ещё висит.

Почему submit-гард бесполезен

Самый частый совет в интернете — повесить обработчик на submit формы и отменить событие, если номер короткий. На страницах Тильды это не работает: submit она отменяет сама и вызывает btn.click(), а валидацию и отправку делает уже обработчик клика по кнопке.

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

Из того же следует, где заканчиваются возможности фронтенда вообще. Проверить, что номер существует, на странице нельзя — для этого нужен запрос на сервер, а значит, своя обработка заявок, а не скрипт в блоке T123.

Что остаётся сделать честно

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

Считать, сколько цифр не хватает, а не «правильный ли номер». Формат страны знает, сколько в национальной части знаков; разница и есть ответ:

function missing(read) {
  var need = size(read.plan) - read.national.length;
  // Длиннее плана — не ошибка: так дописывают добавочный номер.
  return need > 0 ? need : 0;
}

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

Понимать «8» как «+7». С городского телефона номер набирают именно так, и посетитель не обязан знать, чего ждёт ваше поле:

// «8 903...» — это тот же +7: так номер набирают с городского телефона.
if (!plus && home === '7' && digits.charAt(0) === '8') {
  digits = '7' + digits.slice(1);
}

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

Курсор возвращать по счёту цифр, а не символов. После подстановки скобок номер символа сдвинулся; если вернуть курсор на прежнее место, он окажется посреди разделителя. И отдельно — Backspace: если цифры не изменились, а событие было удалением, значит стереть хотели цифру, а разделитель просто оказался на пути. Без этого клавиша упирается в скобку и перестаёт работать — и в консоли об этом, разумеется, ничего.

Писать по-русски. «не хватает 1 цифры», «не хватает 3 цифр», «не хватает 11 цифр» — три разные формы, и человек замечает, когда их не различают.

Всё перечисленное — код, который надо один раз написать и потом не ломать. Он собран в моде из библиотеки, раздел «Формы и квизы», вместе с разбором номера, отказом от чужих полей и надписью в родном контейнере.

Что проверить у себя

  1. Откройте форму на живой странице и посмотрите в инспекторе, есть ли рядом с полем телефона обёртка .t-input-phonemask__wrap. Есть — поле занято маской.
  2. Проверьте, что уходит на сервер. Если маска работает, значение видимого поля идёт без кода страны; на сервер уходит скрытое js-phonemask-result.
  3. Попробуйте отправить «+7 999». Прошло — родной проверки длины у вас нет.
  4. Если решаете маской из редактора — на этом можно остановиться, кода не нужно.
  5. Если пишете свой код — сначала отказ от полей с маской, потом всё остальное.
  6. Сообщение — в .t-input-error, со своей пометкой на группе, чтобы стирать только своё.
  7. Отправку не перехватывайте. Цена ошибки — заявка, а не запись в консоли.
Вопросы по этой статье 4

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