← Назад в блог

Как избавиться от 100vh бага на мобильных через dvh/svh/lvh

Практический гайд: сломанный layout и hero на 100vh, разница vh vs dvh, fallback, поддержка браузеров. Сравнение старого способа (JS resize) и нового (dvh). Контейнерные единицы — один кейс.

Как избавиться от 100vh бага на мобильных через dvh/svh/lvh

Как избавиться от 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 (кратко)

vhdvh
От чего«Большой» viewport (часто без учёта UI браузера)Текущая видимая высота окна
На мобильномПочти не меняется при скроллеМеняется при скрытии/появлении адресной строки
РезультатПрыжки, лишние/обрезанные областиLayout совпадает с тем, что видит пользователь

Один и тот же блок:

/* Плохо на мобильных */
.block { height: 100vh; }

/* Хорошо */
.block { height: 100dvh; }

Для модалок и форм, где важно ничего не обрезать, используй svh (минимальная высота viewport):

.modal {
  max-height: 100svh;
  overflow-y: auto;
}

Поддержка браузеров

Браузерsvh / lvh / dvh
Chrome108+
Edge108+
Firefox101+
Safari15.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 (с fallback 100vh в @supports).
  • Для «полноэкранного» hero без учёта UI браузера — 100lvh.
  • Для модалок и форм — 100svh, чтобы не обрезало.
  • Высоту не трогать в JS — достаточно dvh/svh/lvh.
  • В компонентах (карточки, виджеты) — container-type + cqw/cqh вместо vw/vh.

Так ты убираешь 100vh-баг на мобильных и получаешь предсказуемый layout без костылей.

0 просмотров

Комментарии

Загрузка комментариев...
Пока нет комментариев. Будьте первым!