iOS: как заставить Safari менять viewport при фокусе
Почему iOS‑клавиатура в Safari на iPhone не ресайзит layout‑viewport и как это исправить: Visual Viewport API, вычисление --vh, padding‑bottom, scrollIntoView и fallback‑паттерны.
Как заставить клавиатуру Safari на iPhone изменять размер страницы (viewport) при фокусе на поле ввода? В Safari на iOS при активации input размер страницы не ресайзится (нижнее меню остается прилипшим), в отличие от Android, Chrome и Firefox. Как исправить это поведение?
На iPhone в Safari системная ios клавиатура обычно не меняет layout viewport, поэтому элементы с высотой 100vh и fixed‑нижние панели остаются «под» клавиатурой. Решение — комбинировать meta viewport (viewport‑fit=cover), Visual Viewport API + CSS‑переменную --vh и простые обходы: scrollIntoView с задержкой, scroll‑padding/scroll‑margin и сдвиг footer по вычисленной высоте клавиатуры. Ниже — проверенные паттерны, готовый код и fallbackы для старых iOS.
Содержание
- Почему клавиатура iOS не ресайзит viewport в Safari на iPhone
- Быстрое исправление (минимум изменений)
- Надёжное решение: Visual Viewport API + CSS‑переменная --vh
- Паттерны для fixed‑элементов и форм (реальные примеры)
- Fallbackы и кроссбраузерность (VirtualKeyboard API и старые iOS)
- Тестирование и отладка на iPhone
- Источники
- Заключение
Почему клавиатура iOS не ресайзит viewport в Safari на iPhone
Коротко — Safari на iOS разделяет «layout viewport» и «visual viewport». Когда открывается клавиатура, visual viewport (то, что видно) сжимается, но layout viewport остаётся тем же: значения вроде 100vh и поведение position:fixed опираются на layout, поэтому элементы не «поднимаются» вместе с экраном. Так описано в обсуждениях и практических решениях на StackOverflow и в статьях разработчиков (см. пример на StackOverflow: https://stackoverflow.com/questions/39417778/iphone-safari-not-resizing-viewport-on-keyboard-open).
Результат: input может быть видим, но fixed‑панель на bottom остаётся на прежнем месте и оказывается под клавиатурой. Что ещё хуже — разные версии iOS/браузеров ведут себя по‑разному, поэтому нужно универсальное решение.
Быстрое исправление (минимум изменений)
Если нужно скорое улучшение без глубоких правок, сделайте три вещи:
- meta viewport с safe‑area:
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
- базовый CSS, чтобы не полагаться на 100vh напрямую:
html { height: -webkit-fill-available; }
:root { --vh: 1vh; }
.app { min-height: calc(var(--vh, 1vh) * 100); } /* вместо height:100vh */
.scroll-container { -webkit-overflow-scrolling: touch; overflow-y: auto; }
- простой JS‑fallback: при фокусе прокручиваем input внутрь видимой области с небольшой задержкой:
document.addEventListener('focusin', (e) => {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') {
setTimeout(() => e.target.scrollIntoView({ block: 'center', behavior: 'smooth' }), 250);
}
});
Этого часто достаточно: meta+CSS убирают большинство проблем с 100vh, а scrollIntoView гарантирует видимость поля. Подробнее про такие патчи — статья с примерами CSS/JS: https://medium.com/@krutilin.sergey.ks/fixing-the-safari-mobile-resizing-bug-6568f933cde0.
Надёжное решение: Visual Viewport API + CSS‑переменная --vh
Для корректной работы на iOS 13+ используйте Visual Viewport API — она даёт реальную высоту видимой области. Идея: пересчитать «1vh» из visualViewport, хранить в --vh и применять вместо 100vh; одновременно вычислить высоту клавиатуры как разницу между layout (window.innerHeight) и visualViewport.height.
Пример кода:
function updateVhAndKeyboard() {
const vv = window.visualViewport;
const visibleHeight = vv ? vv.height : window.innerHeight;
// 1vh в пикселях
const vh = visibleHeight * 0.01;
document.documentElement.style.setProperty('--vh', `${vh}px`);
const keyboardHeight = vv ? Math.max(0, window.innerHeight - vv.height) : 0;
document.documentElement.style.setProperty('--keyboard-height', `${keyboardHeight}px`);
}
updateVhAndKeyboard();
window.addEventListener('resize', updateVhAndKeyboard);
if (window.visualViewport) window.visualViewport.addEventListener('resize', updateVhAndKeyboard);
CSS:
/* full-height блоки */
.fullscreen { height: calc(var(--vh, 1vh) * 100); }
/* на контейнер прокрутки можно добавить паддинг, чтобы не прятать элементы */
.scroll-container { padding-bottom: calc(var(--keyboard-height, 0px) + env(safe-area-inset-bottom)); }
/* сдвиг footer через JS (если хотите чисто через CSS, потребуется записывать переменную в root) */
.bottom-bar { position: fixed; left: 0; right: 0; bottom: 0; transition: transform .18s ease; }
Вместо постоянного reliance на 100vh вы теперь подстраиваетесь под реальную видимую область. Пример и обоснование подхода подробно рассмотрены в блоге Брэма (Visual Viewport и VirtualKeyboard): https://www.bram.us/2021/09/13/prevent-items-from-being-hidden-underneath-the-virtual-keyboard-by-means-of-the-virtualkeyboard-api/.
Паттерны для fixed‑элементов и форм (реальные примеры)
Ниже — практические варианты, выбирайте в зависимости от UX и сложности проекта.
- Переключать fixed → absolute внутри прокручиваемого контейнера
- Помещайте footer в контейнер с position: relative; при фокусе делайте footer position: absolute; bottom: var(–keyboard-height); — тогда он окажется над клавиатурой.
- Сдвигать фиксированный footer через transform
- Вычисляете keyboardHeight (см. выше) и ставите footer.style.transform =
translateY(-${keyboardHeight}px); это плавно поднимет панель.
- scroll-padding/scroll-margin для корректных scrollIntoView
.scroll-container { scroll-padding-bottom: calc(var(--keyboard-height, 0px) + 20px); }
input, textarea { scroll-margin-bottom: 20px; }
Это помогает браузеру корректно позиционировать поле при программной прокрутке.
-
Избегайте body { overflow: hidden } и жестких max‑height: это мешает прокрутке и работе visualViewport.
-
Паддинг для форм: если динамически добавлять padding‑bottom равный keyboardHeight — поля не будут скрываться.
Пример общего подхода:
function onFocusAdjust(e) {
const kb = parseInt(getComputedStyle(document.documentElement).getPropertyValue('--keyboard-height')) || 0;
const sc = document.querySelector('.scroll-container');
sc.style.paddingBottom = `${kb}px`;
setTimeout(() => e.target.scrollIntoView({ block: 'center' }), 200);
}
document.addEventListener('focusin', onFocusAdjust);
Fallbackы и кроссбраузерность (VirtualKeyboard API и старые iOS)
VirtualKeyboard API (в современных Chromium) предоставляет события/метрики клавиатуры и облегчит задачу, но Safari пока не реализовал этот API полностью. Поэтому делаем feature‑detection и fallback:
- Если есть navigator.virtualKeyboard → используйте его (в Chrome/Android) для чтения размеров/geometry.
- Иначе — используйте window.visualViewport (iOS 13+).
- Для старых iOS без visualViewport используйте простой focusin + setTimeout + scrollIntoView или фиксированную подгонку (например, добавлять padding‑bottom ~300px) — это грязно, но работает в большинстве случаев.
Пример проверки:
if ('virtualKeyboard' in navigator) {
// handle with VirtualKeyboard API where available
} else if (window.visualViewport) {
// visualViewport approach
} else {
// fallback: timeout + scrollIntoView
}
Для русскоязычных заметок о таких обходах см. обсуждение на Хабре: https://qna.habr.com/q/522091 и старые ветки StackOverflow (https://stackoverflow.com/questions/23940438/want-to-have-browser-viewport-resize-when-ios-keyboard-is-activated).
Тестирование и отладка на iPhone
Как отлаживать? Несколько советов:
- Тестируйте на реальных устройствах. Эмуляторы часто ведут себя иначе.
- Используйте Safari Web Inspector (Mac): Develop → [iPhone] → Inspect. Вы увидите window.visualViewport и window.innerHeight в реальном времени.
- Логируйте значения при focusin/focusout:
document.addEventListener('focusin', () => {
console.log('innerHeight', window.innerHeight, 'visual', window.visualViewport && window.visualViewport.height);
});
- Пробуйте разные сценарии: с адресной строкой видимой/скрытой, в портрете и ландшафте, с внешней клавиатурой (она меняет поведение).
- Помните: тайминги открытия клавиатуры могут варьироваться — экспериментируйте с задержкой 200–400 ms для scrollIntoView.
Небольшая проверка: если при открытии клавиатуры window.innerHeight не меняется, а window.visualViewport.height уменьшается — ваш браузер ведёт себя как Safari и нуждается в visualViewport/–vh патче.
Источники
- iPhone Safari not resizing viewport on keyboard open — Stack Overflow
- Fixing the Safari Mobile Resizing Bug: A Developer’s Guide — Medium
- Prevent items from being hidden underneath the Virtual Keyboard — Bram.us
- Want to have browser viewport resize when iOS keyboard is activated — Stack Overflow
- Как узнать высоту клавиатуры и нижнего бара на iOS Safari? — Habr Q&A
Заключение
Коротко: Safari на iPhone не меняет layout viewport — поэтому нужно имитировать ресайз через Visual Viewport API + CSS‑переменную --vh и подстраивать fixed‑элементы (или временно переключать их поведение). Для старых устройств ставьте fallback: scrollIntoView с задержкой и добавление padding‑bottom. С этим набором приёмов поведение ios клавиатуры в Safari станет предсказуемым и пользовательский интерфейс — удобным.