Адаптивность в CSS и JavaScript: типичные ошибки вёрстки
Почему макет разъезжается на телефоне, resize срабатывает 60 раз в секунду, а 100vh обрезает контент в Safari. Разбор частых ошибок адаптивной вёрстки и рабочие замены.

Кнопка «Купить» на iPhone уезжает под панель браузера, горизонтальный скролл появляется от одного лишнего width: 100%, а обработчик resize вешает интерфейс при повороте экрана. Это не редкие баги — это одни и те же ошибки, которые повторяются в проекте за проектом. Ниже — разбор самых частых из них: что именно ломается, чем платите и чем заменить. Актуально для современных браузеров (Chrome/Firefox/Safari 2023–2024 годов), где поддержаны dvh, контейнерные запросы и ResizeObserver.
Ошибка 1. Фиксированные размеры вместо гибких
Классика: элементу задают width: 400px, потому что на макете он именно такой. На широком экране всё хорошо, а на 360px появляется горизонтальный скролл — блок шире вьюпорта, и страница ползёт вбок.
Проблема не в самой единице px, а в том, что размер жёсткий и не знает про доступное место. Замена — ограничить максимум, а не зафиксировать значение:
/* было: разъедется на узком экране */
.card {
width: 400px;
}
/* стало: занимает доступную ширину, но не больше 400px */
.card {
width: 100%;
max-width: 400px;
}
Отдельный источник горизонтального скролла — width: 100% у блока с padding или border без box-sizing: border-box. Тогда 100% ширины родителя плюс отступы дают ширину больше родителя. Глобальное правило * { box-sizing: border-box; } закрывает этот класс проблем целиком.
Ошибка 2. Медиазапросы по устройствам, а не по контенту
Точки перелома вида @media (max-width: 768px) «под iPad» — ловушка. Устройств стало столько, что подгонять брейкпоинты под конкретные модели бессмысленно: завтра выйдет экран другой ширины, и вёрстка снова поедет.
Правильный критерий — контент. Смотрите, при какой ширине конкретный блок начинает выглядеть плохо (текст слишком длинный, карточки жмутся), и ставьте перелом туда. Часто адаптив вообще не требует медиазапросов:
/* карточки сами переносятся, когда перестают влезать */
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 16px;
}
Здесь auto-fit и minmax держат минимальную ширину карточки в 240px и раскладывают столько колонок, сколько влезает. Ни одного медиазапроса — и работает на любой ширине.
Контейнерные запросы, когда важна ширина родителя, а не экрана
Одна карточка в сайдбаре и та же карточка на всю ширину — разные ситуации, а медиазапрос знает только ширину вьюпорта. Контейнерные запросы (@container) реагируют на ширину родителя. Поддержаны во всех основных браузерах примерно с 2023 года — проверьте caniuse, если у вас требования к старым версиям.
Ошибка 3. 100vh на мобильных
Экран приветствия на всю высоту делают через height: 100vh. На десктопе всё хорошо, а на мобильном Safari нижняя часть контента прячется под адресную строку браузера. Причина: vh считается от полной высоты вьюпорта без учёта сворачивающихся панелей браузера.
Решение — динамические единицы dvh (dynamic viewport height), которые учитывают, свёрнута панель или нет:
.hero {
min-height: 100dvh; /* учитывает панели браузера */
min-height: 100vh; /* фолбэк для старых браузеров: ставьте ПЕРВЫМ */
}
Порядок важен: старое правило100vhставьте выше, а100dvhниже. Браузер, который не знает проdvh, проигнорирует непонятную строку и оставитvh. Браузер, который знает, применит последнее валидное правило.
Есть ещё svh (наименьшая высота, когда панели раскрыты) и lvh (наибольшая). Для секции на весь экран без «прыжков» контента при скролле иногда стабильнее взять svh.
Ошибка 4. resize без ограничения частоты
Самая дорогая ошибка в JS-части адаптива — тяжёлый обработчик на событии resize. При растягивании окна мышью событие срабатывает десятки раз в секунду, и если внутри пересчёт лейаута или запрос в DOM, интерфейс начинает подтормаживать.
// плохо: пересчёт на каждом кадре перетаскивания
window.addEventListener('resize', () => {
пересчитатьРаскладку(); // может вызываться 60 раз в секунду
});
Замена — debounce: выполнять обработчик один раз, когда пользователь перестал менять размер:
let таймер;
window.addEventListener('resize', () => {
clearTimeout(таймер);
таймер = setTimeout(пересчитатьРаскладку, 150); // сработает через 150 мс покоя
});
Если нужно реагировать на изменение размера конкретного элемента, а не всего окна, лучше вообще уйти от resize к ResizeObserver — он вызывает колбэк только при реальном изменении наблюдаемого элемента и не завязан на вьюпорт:
const наблюдатель = new ResizeObserver((записи) => {
for (const запись of записи) {
const ширина = запись.contentRect.width;
// реагируем на новую ширину элемента
}
});
наблюдатель.observe(document.querySelector('.card'));
Ошибка 5. Определять устройство по ширине или User-Agent
Код вида if (window.innerWidth < 768) { /* это телефон */ } ошибается на планшетах, на десктопе с узким окном и на устройствах с тач-экраном и большим монитором. Ширина окна не равна типу ввода.
Если нужно узнать именно способ ввода (тач или мышь), спрашивайте про способ ввода, а не про размер:
// есть ли грубый указатель (палец) как основной ввод
const этоТач = window.matchMedia('(pointer: coarse)').matches;
Парсинг navigator.userAgent для определения устройства — путь в постоянные правки: строки UA меняются, эмуляторы врут, а новые устройства ваш регэксп не знает. Опирайтесь на возможности (feature detection), а не на модель.
Ошибка 6. Растровые картинки без srcset
Одна и та же картинка 2000px шириной грузится и на десктоп, и на телефон с экраном 360px. На мобильном трафике это лишние мегабайты и медленная загрузка. Атрибут srcset отдаёт браузеру выбор подходящего размера:
<img
src="photo-800.jpg"
srcset="photo-400.jpg 400w, photo-800.jpg 800w, photo-1600.jpg 1600w"
sizes="(max-width: 600px) 100vw, 800px"
alt="описание">
Браузер сам возьмёт версию под ширину экрана и плотность пикселей. Атрибут sizes подсказывает, какую площадь картинка займёт при разной ширине вьюпорта, — без него браузер считает, что всегда 100vw, и может взять версию крупнее нужной.
Сравнение: что чем заменить
| Ошибка | Чем плоха | Замена |
|---|---|---|
Жёсткий width: Npx | Горизонтальный скролл на узком экране | width: 100%; max-width: N |
| Брейкпоинты под устройства | Ломается на новых экранах | Перелом по контенту, auto-fit |
100vh на мобильных | Контент под панелью браузера | 100dvh с фолбэком |
Тяжёлый resize | Подтормаживания интерфейса | debounce или ResizeObserver |
| Определение по ширине/UA | Ошибки на планшетах и тач-десктопах | matchMedia('(pointer: coarse)') |
| Картинка одного размера | Лишний трафик на мобильном | srcset + sizes |
Что проверить в своём проекте прямо сейчас
- Откройте DevTools, включите режим устройства и поставьте ширину 320px — если появился горизонтальный скролл, ищите блок с жёсткой шириной или
100%безborder-box. - Проверьте секции на весь экран в мобильном Safari: не прячется ли низ под панель. Если да — переводите на
dvh. - Найдите все
addEventListener('resize'и посмотрите, что внутри: тяжёлую логику оберните в debounce. - Прогоните страницу через Lighthouse в Chrome — он отдельно укажет на картинки без адаптивных размеров и на элементы шире вьюпорта.
Инвалидация кеша адаптивных изображений и вёрстка под печать — отдельные темы, здесь их не разбираем. Следующий шаг после чистки этих ошибок — посмотреть на CLS (сдвиг макета) в поле: часто именно адаптивные изображения без явных width/height дают скачки при загрузке.
Контент-конвейер: Из одной идеи — текст, изображение, озвучка и ролик. Один заказ вместо четырёх.
Спросить за 60 ₽Источники: MDN: CSS values and units — viewport units (dvh, svh, lvh), MDN: ResizeObserver API
Частые вопросы
У меня в проекте нужно поддерживать старые браузеры без dvh. Что делать?
Контейнерные запросы у меня не работают, хотя браузер свежий.
Debounce на resize добавляет задержку. Как убрать эффект «дёргания» лейаута?
Можно ли определить именно мобильное устройство, а не тип ввода?
srcset настроил, но браузер всё равно грузит крупную версию.
Почему появляется горизонтальный скролл, хотя явных широких блоков нет?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.