Привычные viewport-единицы появились в CSS ещё в 2012 году. Конструкция 100vh для секции в полный экран — настолько распространённый паттерн, что его пишут не задумываясь. И всё бы хорошо, если бы не одна досадная мелочь: на мобильных браузерах 100vh работает не так, как ожидаешь. Контент режется снизу, центрирование съезжает, а полноэкранная модалка не показывает кнопку «закрыть».

В 2022 году CSS Working Group добавили новый набор единиц — svh, lvh и dvh (плюс их аналоги для ширины и логических осей). К 2024-му они стали Baseline-широкоподдержанными, и тянуть с переходом больше нет смысла. Разберёмся, в чём именно проблема старого 100vh, что значат три новые буквы перед vh, и какие задачи они закрывают на практике.

В чём проблема 100vh на мобильном

На десктопе размер окна браузера фиксирован: пользователь не открывает и не закрывает свой address bar по ходу скролла. На мобильном — ровно наоборот. iOS Safari и Chrome для Android прячут адресную строку и нижнюю панель навигации, когда пользователь начинает листать вниз: освобождают экран ради контента. Когда пользователь скроллит наверх — панели возвращаются.

Высота viewport в этих двух состояниях разная. Старая единица vh вычисляется один раз и фиксируется на большем из двух значений — том, что получится после скрытия панелей. То есть когда страница только открылась и панели ещё на экране, элемент с height: 100vh на самом деле выше видимой области, и низ контента уезжает за нижнюю панель.

Самый болезненный сценарий — полноэкранный загрузочный экран:

.loader {
  position: fixed;
  inset: 0 0 auto 0;
  height: 100vh;
  display: grid;
  place-items: center;
}

Иконка лоадера якобы по центру по CSS, а визуально смещена вниз — потому что центр считается от высоты, которая больше реально видимой. Та же история со sticky-футером в модалке (его не видно), с CTA-кнопкой в полноэкранной hero-секции (она частично за нижней панелью), с любым layout, где центр или низ обязан быть в кадре.

Сравнение реализации полноэкранного загрузочного экрана

Малый, большой и динамический viewport

Решение спецификации CSS Values 4 — разделить понятие viewport на три состояния, каждому соответствует свой префикс перед vh:

svh — small viewport

Высота viewport, когда browser-bars максимально развёрнуты. То есть самое маленькое значение из всех возможных. Полноэкранный элемент с height: 100svh гарантированно поместится в видимую область даже при показанной адресной строке — и при скролле не будет смещаться.

lvh — large viewport

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

Схема: svh — высота при показанных панелях, lvh — высота при свёрнутых панелях

dvh — dynamic viewport

Динамическая высота: браузер пересчитывает её на лету в зависимости от текущего состояния панелей. Когда адресная строка свёрнута — dvh равен lvh; когда развёрнута — svh. По дороге между этими состояниями элемент «дышит» вместе с UI браузера.

Звучит как универсальное решение — и иногда так и есть, — но у динамического пересчёта есть нюансы, к которым вернёмся в отдельном разделе ниже.

Не только высота

Важная деталь, которую часто упускают в обзорах: новых единиц на самом деле не три, а целая сетка. Префиксы s, l, d применимы ко всем классическим viewport-единицам, не только к высоте.

Тип Small (s*) Large (l*) Dynamic (d*)
Высота svh lvh dvh
Ширина svw lvw dvw
Меньшая сторона svmin lvmin dvmin
Бо́льшая сторона svmax lvmax dvmax

Дополнительно есть логические аналоги svi/lvi/dvi (inline-направление текста) и svb/lvb/dvb (block-направление). Они полезны при writing-mode: vertical-rl, поддержке RTL-вёрстки или вертикальной восточноазиатской типографики — там, где «высота» и «ширина» зависят от направления письма. Подробный разбор самого свойства writing-mode и того, когда оно нужно — в отдельной статье.

Где это реально пригождается

Загрузочный экран

Тот самый кейс из вступления. Замена vh на svh убирает съезжающее центрирование одной строкой:

.loader {
  position: fixed;
  inset: 0 0 auto 0;
  height: 100svh;
  display: grid;
  place-items: center;
}

Модалка со sticky-хэдером и футером

Полноэкранная модалка, у которой шапка и подвал прибиты, а в середине скроллящийся контент. Если использовать 100vh, на мобильном Safari нижний футер уезжает за адресную строку, и пользователь не видит кнопку «Закрыть».

.modal {
  position: fixed;
  inset: 0;
  height: 100svh;
  display: grid;
  grid-template-rows: auto 1fr auto;
}

.modal__header,
.modal__footer { position: sticky; }
.modal__body { overflow-y: auto; }

С 100svh футер гарантированно в кадре. Альтернатива — 100dvh, тогда модалка будет немного увеличиваться при скрытии адресной строки. Что выбрать — вопрос вкуса; в пользу svh — стабильное положение элементов без «дыхания».

Hero-секция за вычетом фиксированного хэдера

Классическая задача: главная секция должна занять всю оставшуюся после хэдера высоту экрана. Стандартное решение через calc() теперь использует новую единицу:

:root {
  --header-height: 60px;
}

.site-header {
  position: sticky;
  top: 0;
  min-height: var(--header-height);
}

.hero {
  min-height: calc(100svh - var(--header-height));
}

На десктопе изменений не будет, на мобильном — пропадёт обрезка декора у нижнего края секции, который раньше прятался за нижней панелью браузера.

dvh как универсальный дефолт — не лучшая идея

Соблазн заменить все vh на dvh в проекте сразу понятный: пусть всё подстраивается. На практике у этой замены есть две проблемы.

Скачущая типографика. Если задать font-size через dvh, размер шрифта будет меняться по ходу скролла — в момент сворачивания/разворачивания адресной строки. Выглядит это как глитч:

/* так делать не надо */
h1 {
  font-size: calc(1rem + 5dvh);
}

Для свойств, у которых пересчёт виден глазу — font-size, padding, gap, transform, — dvh противопоказан. Для них берём svh: значение зафиксируется по нижней границе и не будет дёргаться.

Производительность. Динамический пересчёт стилей при скролле — работа для движка. На простой странице это незаметно, но на тяжёлом layout с большим количеством dvh-зависимых свойств можно заметить просадку плавности скролла. Точные замеры зависят от страницы; общее правило — не использовать dvh там, где достаточно svh.

Фолбэк для очень старых браузеров

Несмотря на то, что новые единицы давно Baseline, всё ещё могут попадаться браузеры до 2022 года — чаще всего на старых Android-устройствах. Удобный паттерн фолбэка использует тот факт, что незнакомое CSS-значение просто игнорируется:

.hero {
  height: 100vh;     /* старый движок прочитает только это */
  height: 100svh;    /* новый движок переопределит */
}

Если хочется явной фича-детекции с разной логикой — есть @supports:

@supports (height: 100svh) {
  .hero { height: 100svh; }
}

Но в большинстве проектов хватает простого каскадного подхода выше.

Поддержка браузерами

Поддержка браузерами
chrome
Chrome
108
firefox
Firefox
101
edge
Edge
108
safari
Safari
15.4
opera
Opera
94

Итог

Краткое практическое правило для повседневной верстки:

  • Полноэкранные секции, модалки, загрузочные экраны — берите 100svh. Это самый частый и самый безопасный выбор.
  • Если по дизайну важно, чтобы блок «дышал» вместе с UI браузера — 100dvh. Но не для свойств, где скачки заметны.
  • 100lvh на практике эквивалентен старому 100vh и в новой верстке не нужен.
  • Для font-size, отступов и других визуальных свойств — только svh или совсем без viewport-единиц.
  • Фолбэк для старых движков — парная декларация height: 100vh; height: 100svh;.

Реальную разницу между всеми тремя единицами лучше всего смотреть на телефоне — на десктопе address bar не сворачивается, и все три значения визуально совпадают. Если контейнерные запросы и адаптивный дизайн в целом всё ещё в стадии освоения — начните с обзорной статьи про CSS Container Queries: новые viewport-единицы и контейнерные запросы — две части одной общей истории про адаптацию вёрстки к реальному окну.