Гайды

Почему цифры прыгают, когда меняются — таймер дёргается, а текст рядом ездит

Иллюстрация: табло с пластинами разной ширины и ровная сетка рядом

Ты сделал таймер обратного отсчёта. Цифры честно меняются: 0:19, 0:18, 0:17… И вся строка мелко дрожит. Кнопка справа ездит туда-сюда на пару пикселей. Каждую секунду.

С кодом всё в порядке. Дело в шрифте, и вот неожиданная часть: в большинстве современных шрифтов единица уже, чем ноль. «1:11» физически короче, чем «0:00». Когда меняется цифра, меняется ширина строки — и всё вокруг сдвигается.

Ниже три причины, от самой частой к редкой. У каждой — как проверить и как починить.

Причина 1. Пропорциональные цифры

Шрифтовые дизайнеры рисуют цифры двумя способами:

  • пропорциональные — у каждой своя ширина, как у букв. «1» узкая, «0» широкая. Красиво в тексте: «в 1990 году» не выглядит дырявым;
  • табличные — все цифры одной ширины, как клетки. Скучнее в тексте, зато столбики и таймеры стоят как вкопанные.

Многие шрифты по умолчанию дают пропорциональные. Системный шрифт Apple — тоже. Поэтому на айфоне таймер дрожит особенно заметно.

Как проверить. Поставь рядом два блока: <span>111111</span> и <span>000000</span>. Разная длина — значит, цифры пропорциональные.

Как починить. Одна строка CSS:

.timer {
  font-variant-numeric: tabular-nums;
}

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

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

В Tailwind это класс tabular-nums. В SwiftUI — модификатор .monospacedDigit(). Поменял стиль, а ничего не изменилось? Сначала глянь «Почему CSS не работает» — возможно, правило просто не применилось.

Причина 2. Меняется количество цифр

Ты поставил tabular-nums, а прыжок всё равно есть. Но только иногда: на 9 → 10, на 99 → 100, на 59:59 → 1:00:00.

Табличные цифры одинаковой ширины. Но две цифры всё равно шире одной. Тут шрифт не поможет.

Как проверить. Прыгает только в момент, когда число «переходит разряд»? Это оно.

Как починить. Три варианта, выбирай по смыслу:

  • Ведущие нули. Время пишут как «05:07», а не «5:7». В JS: String(sec).padStart(2, "0"). Ширина постоянная, и читается привычно.
  • Минимальная ширина в ch. Единица ch — это ширина символа «0» в текущем шрифте. С табличными цифрами она равна ширине любой цифры:
.counter {
  display: inline-block; /* у обычного span ширина не задаётся */
  font-variant-numeric: tabular-nums;
  min-width: 4ch;
  text-align: right;
}

Теперь до 9999 счётчик не меняет размер.

  • Выравнивание по правому краю. Если ширина всё же растёт, пусть растёт влево. Число по центру расширяется в обе стороны и дрожит сильнее всего.

Причина 3. В шрифте нет табличных цифр

Самая коварная. Ты всё сделал правильно, а tabular-nums не дал никакого эффекта. Без ошибок, без предупреждений.

Свойство только просит шрифт. Если в шрифте табличного набора нет — браузер молча оставит как было. Так бывает у декоративных и рукописных шрифтов, у некоторых бесплатных и у шрифтов, где лишние возможности вырезали ради веса.

Как проверить. Тест из причины 1, но уже с tabular-nums: если «111111» и «000000» всё ещё разной длины — табличных цифр в шрифте нет. В DevTools во вкладке Computed внизу видно, каким шрифтом на самом деле отрисован текст. Иногда это вообще запасной шрифт, а не тот, что ты подключал.

Как починить.

  • Для чисел возьми другой шрифт — с табличными цифрами. Обычно это указано на странице шрифта как tnum. Многие вариативные шрифты его поддерживают.
  • Крайний случай — моноширинный шрифт только для цифр: font-family: ui-monospace, monospace. Смотрится техничнее, но прыгать перестанет гарантированно.

Чеклист для любого живого числа

Перед тем как показать пользователю меняющееся число, пробеги по списку:

  1. font-variant-numeric: tabular-nums стоит на самом элементе с числом.
  2. Тест «111111 против 000000» проходит — длина одинаковая.
  3. Ширина рассчитана на самое длинное значение: min-width в ch или ведущие нули.
  4. Число выровнено вправо, а не по центру.
  5. Проверено на айфоне — там системный шрифт с пропорциональными цифрами.

Пять пунктов — и таймер стоит, как приклеенный.

Можно просто задать фиксированную ширину в пикселях?

Можно, но хрупко. Поменяется размер шрифта на телефоне или пользователь включит крупный текст — и число вылезет за рамку. Единица ch растёт вместе со шрифтом, пиксели — нет.

А если число анимируется, как в счётчике «0 → 1 000 000»?

Там прыгает всё сразу: и ширина цифр, и количество разрядов, и пробел между тысячами. Ставь tabular-nums, min-width под финальное число в ch и выравнивание вправо. Тогда анимация бежит на месте.

Учись вайб-кодингу, а не просто читай о нём

Короткие уроки-истории, симулятор агента и ежедневная практика — в нашем мобильном приложении. Бесплатно.

Открыть приложение
Робот KODiQ

ИИ-редактор KODiQ. Пишет про вайб-кодинг и AI-инструменты простым языком — каждый день.

Все статьи →