Гайды

Почему 100vh выше экрана на телефоне — 3 причины и как починить

Иллюстрация: высокая золотая плита не помещается в окно телефона и выглядывает снизу, Кодик меряет её рулеткой

Ты сделал первый экран на height: 100vh. Заголовок, картинка, внизу кнопка «Начать». На компьютере — идеально. На телефоне кнопки нет. Она появляется, только если чуть прокрутить страницу.

И самое обидное: в режиме телефона в DevTools всё выглядит нормально. На странице web.dev об этом сказано прямо: в браузерах без выезжающих панелей, как Chrome на компьютере, все варианты высоты экрана совпадают. Баг живёт только на настоящем телефоне.

Вот три причины — от самой частой к редкой.

Причина 1. vh считается по «большому» экрану

У мобильного браузера две высоты экрана. Пока адресная строка видна — экран поменьше. Когда ты листаешь вниз и строка прячется — побольше.

vh берёт большую. Chrome на Android перешёл на это ещё в версии 56, в 2017 году, — чтобы вести себя так же, как Safari на iPhone. Команда Chrome так и написала: 100vh будет больше видимой высоты, пока адресная строка на экране.

Получается: при загрузке строка видна, а блок рассчитан так, будто её нет. Низ блока уходит за край.

Как проверить. Открой сайт на телефоне и чуть прокрути. Если кнопка выехала ровно в тот момент, когда спряталась адресная строка, — это она.

Как починить. С 2022 года во всех основных браузерах есть новые единицы (Chrome 108, Firefox 101, Safari 15.4):

  • svh — маленький экран, панели браузера видны;
  • lvh — большой экран, панели спрятаны. Это старый добрый vh;
  • dvh — динамическая высота: меняется, когда панели выезжают и прячутся.

Для первого экрана бери svh:

.hero {
  min-height: 100vh;
  min-height: 100svh;
}

Первая строка — запасная для старых браузеров. Вторая её перекрывает там, где svh понимают. А min-height вместо height позволяет блоку вырасти, если текст не влез.

Почему не dvh? Он меняется вместе с панелями: пока человек листает, блок растёт и сжимается, а всё под ним сдвигается. К тому же web.dev предупреждает, что динамическая высота обновляется не на каждом кадре, а с задержкой. Оставь dvh для полноэкранных интерфейсов вроде чата, где высота должна совпадать точно.

В Tailwind класс h-screen — это как раз 100vh. Замени его на min-h-svh или h-dvh: эти классы есть с версии 3.4.

Причина 2. Клавиатура закрывает низ

Строка ввода чата закреплена внизу экрана. Нажимаешь на неё — выезжает клавиатура и закрывает поле.

web.dev объясняет: экранная клавиатура не считается панелью браузера и не меняет единицы высоты. А с Chrome 108 клавиатура на Android уменьшает только видимую область, но не макет страницы. Так же давно работает Safari на iPhone.

Как проверить. Тапни в поле внизу. Если оно остаётся под клавиатурой — причина здесь.

Как починить. В Chrome на Android можно попросить старое поведение одним ключом в meta-теге:

<meta name="viewport" content="width=device-width, initial-scale=1, interactive-widget=resizes-content">

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

const vv = window.visualViewport;

function syncHeight() {
  document.documentElement.style.setProperty('--app-height', `${vv.height}px`);
}

vv.addEventListener('resize', syncHeight);
syncHeight();

А в CSS контейнер чата получает height: var(--app-height, 100dvh).

Причина 3. Низ уходит под полоску жестов

Кнопка вроде на экране, но наполовину под полоской «домой» на iPhone или под навигационной полосой Android.

Так бывает, если в meta-теге стоит viewport-fit=cover. Страница растягивается до самых краёв, и браузер перестаёт отодвигать её от системных элементов. С версии 135 так умеет и Chrome на Android.

Как проверить. Найди в <meta name="viewport"> значение viewport-fit=cover. Нет его — эта причина не твоя.

Как починить. Добавь нижний отступ из безопасной зоны:

.bottom-bar {
  padding-bottom: max(16px, env(safe-area-inset-bottom));
}

Подробно, что это за отступы и откуда они берутся, — в статье что такое safe area.

Если просишь ИИ сверстать мобильный экран, добавь в запрос одну строку: «не используй 100vh для высоты — бери 100svh с запасным 100vh, учти safe area». Первая и третья причины закрыты заранее. Базовая мобильная настройка — в статье как сделать сайт адаптивным.

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

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

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

Нужен ли ещё старый хак с --vh и innerHeight?

Для обычной высоты экрана — нет. Его придумали, когда единиц svh и dvh не было. Сейчас хватает запасной строки с 100vh. JavaScript остаётся нужен только для клавиатуры на iPhone.

Почему у position: fixed с height: 100% такой проблемы нет?

Команда Chrome описывала это прямо: закреплённый элемент с высотой 100% всегда занимает ровно видимую высоту и меняется вместе с адресной строкой. Это удобно для меню и модальных окон. Но первый экран живёт в обычном потоке страницы — ему нужен svh.

svh или dvh — что ставить по умолчанию?

svh. Блок никогда не уйдёт под панели и не будет дёргаться при прокрутке. dvh — только там, где интерфейс обязан занимать ровно видимую высоту.

А если стили не применяются вообще?

Тогда дело не в vh. Проверь, доходит ли правило до элемента, по списку почему CSS не работает.

Робот KODiQ

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

Все статьи →