Как избавиться от 100vh бага на мобильных через dvh/svh/lvh
Практический гайд: сломанный layout и hero на 100vh, разница vh vs dvh, fallback, поддержка браузеров. Сравнение старого способа (JS resize) и нового (dvh). Контейнерные единицы — один кейс.
Как избавиться от 100vh бага на мобильных через dvh/svh/lvh
На мобильных 100vh ведёт себя непредсказуемо: адресная строка то скрывается, то появляется — высота «видимого» экрана меняется, а vh в старых браузерах привязан к большому viewport. В итоге layout прыгает, hero обрезается или появляются пустые полосы. Ниже — примеры сломанного и исправленного кода, разница vh и dvh, fallback, поддержка браузеров и один практический кейс с container units.
Пример сломанного layout (100vh)
Такой разметки и стилей достаточно, чтобы увидеть баг на телефоне при скролле (адресная строка скрывается/появляется):
<div class="page">
<header class="header">Шапка</header>
<main class="main">Контент</main>
<footer class="footer">Подвал</footer>
</div>
.page {
min-height: 100vh; /* баг: на мобильных не совпадает с реальной высотой экрана */
display: flex;
flex-direction: column;
}
.header { flex-shrink: 0; padding: 1rem; }
.main { flex: 1; padding: 1rem; }
.footer { flex-shrink: 0; padding: 1rem; }
Что не так: на iOS/Android при скролле «видимая» высота меняется, а 100vh остаётся фиксированным (часто больше видимой области). Итог: то пустое пространство снизу, то контент «прыгает».
Исправление: заменить на 100dvh (ниже — с fallback).
Пример hero-блока: сломанный и рабочий
Сломанный вариант (100vh):
.hero {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
padding: 2rem;
}
.hero__title {
font-size: clamp(1.5rem, 4vw, 3rem);
color: #fff;
}
На мобильном при появлении/скрытии UI браузера высота hero «дергается», снизу может обрезаться или появляться полоса.
Рабочий вариант (dvh или lvh):
.hero {
min-height: 100dvh; /* подстраивается под реальную высоту */
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
padding: 2rem;
}
.hero__title {
font-size: clamp(1.5rem, 4vw, 3rem);
color: #fff;
}
Если нужен именно «максимальный» экран (как будто UI браузера скрыт) — используй 100lvh:
.hero {
min-height: 100lvh;
/* ... */
}
Разница vh и dvh (кратко)
vh | dvh | |
|---|---|---|
| От чего | «Большой» viewport (часто без учёта UI браузера) | Текущая видимая высота окна |
| На мобильном | Почти не меняется при скролле | Меняется при скрытии/появлении адресной строки |
| Результат | Прыжки, лишние/обрезанные области | Layout совпадает с тем, что видит пользователь |
Один и тот же блок:
/* Плохо на мобильных */
.block { height: 100vh; }
/* Хорошо */
.block { height: 100dvh; }
Для модалок и форм, где важно ничего не обрезать, используй svh (минимальная высота viewport):
.modal {
max-height: 100svh;
overflow-y: auto;
}
Поддержка браузеров
| Браузер | svh / lvh / dvh |
|---|---|
| Chrome | 108+ |
| Edge | 108+ |
| Firefox | 101+ |
| Safari | 15.4+ |
В 2025–2026 можно смело использовать в проде. Для старых Safari/Android нужен fallback.
Fallback-стратегия
Сначала задаёшь значение для старых браузеров, затем переопределяешь через @supports:
.page {
min-height: 100vh; /* fallback для старых браузеров */
}
@supports (height: 100dvh) {
.page {
min-height: 100dvh;
}
}
Один блок для hero:
.hero {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
@supports (height: 100dvh) {
.hero {
min-height: 100dvh;
}
}
Модалка с svh:
.modal {
max-height: 100vh;
overflow-y: auto;
}
@supports (height: 100svh) {
.modal {
max-height: 100svh;
}
}
Старый способ (JS resize) vs новый (dvh)
Раньше подстраивали высоту под реальный viewport через JS:
<div class="hero" id="hero">Hero</div>
<script>
function setHeight() {
const h = window.visualViewport?.height ?? window.innerHeight;
document.getElementById('hero').style.minHeight = h + 'px';
}
setHeight();
window.visualViewport?.addEventListener('resize', setHeight);
window.addEventListener('resize', setHeight);
</script>
Минусы: лишний JS, подписки на события, возможные мерцания и рассинхрон с отрисовкой.
Сейчас достаточно CSS:
.hero {
min-height: 100dvh;
}
Никакого JS, браузер сам обновляет значение при изменении viewport. Для hero, layout и модалок — один стиль.
Container queries: практический кейс
Карточка в сетке: на широком экране — несколько колонок, на узком — одна. Нужно, чтобы размер заголовка и высота превью зависели от ширины карточки, а не окна.
<article class="card">
<div class="card__image"></div>
<h2 class="card__title">Заголовок</h2>
<p class="card__text">Текст карточки.</p>
</article>
.card {
container-type: inline-size;
container-name: card;
border: 1px solid #e0e0e0;
border-radius: 12px;
overflow: hidden;
}
.card__image {
width: 100%;
height: 20cqw; /* высота от ширины контейнера */
background: #eee;
}
.card__title {
font-size: clamp(1rem, 5cqw, 1.5rem);
padding: 0.75rem 1rem 0 1rem;
}
.card__text {
font-size: clamp(0.875rem, 2.5cqw, 1rem);
padding: 0.5rem 1rem 1rem 1rem;
}
cqw = 1% ширины контейнера. Карточка в три колонки и в одну колонку выглядит пропорционально без медиа-запросов по ширине экрана.
Краткий чеклист
- Вместо
100vhдля страницы и hero —100dvh(с fallback100vhв@supports). - Для «полноэкранного» hero без учёта UI браузера —
100lvh. - Для модалок и форм —
100svh, чтобы не обрезало. - Высоту не трогать в JS — достаточно dvh/svh/lvh.
- В компонентах (карточки, виджеты) — container-type + cqw/cqh вместо vw/vh.
Так ты убираешь 100vh-баг на мобильных и получаешь предсказуемый layout без костылей.



Комментарии