Суть проблемы
Лендинг свёрстан «под десктоп» с фиксированной шириной 1200px. Вёрстку тестировали только на широком мониторе — и она там действительно выглядела идеально. Клиент открыл сайт с iPhone и увидел кашу: всё, что должно было подстраиваться под экран, оставалось «десктопным» и масштабировалось браузером целиком.
- !Горизонтальный скролл и обрезка контентаНа телефоне страница уезжала вправо примерно на 840px, справа видна пустая полоса, контент обрезан.
- !Текст выходит за края экранаЗаголовки и абзацы с длинными URL и неразрывными словами вылезали за правую границу вьюпорта.
- !Кнопки «Купить» наползают на ценуВ карточках тарифов кнопка с
position: absoluteперекрывала цену и соседние блоки. - !Бургер-меню не открывается по тапуИконка видна, но тап ничего не делает — меню остаётся закрытым.
- !На десктопе всё корректноПроблема воспроизводится только на мобильных устройствах и в эмуляции вьюпорта.
Контент шире вьюпорта, появляется горизонтальный скролл ~840px, блоки разъезжаются.
Все блоки укладываются во вьюпорт, скролл строго вертикальный, таппоинты отзывчивы.
Как диагностировал
Сначала воспроизвёл проблему в контролируемой среде, затем пошёл от визуальных симптомов к корневым причинам — каждая находка объясняла сразу несколько симптомов.
- Шаг 1. Эмуляция вьюпортаОткрыл Chrome DevTools, включил
Toggle Device Toolbar(iPhone 12 / SE). Сразу увидел горизонтальный скролл и общееoverflowпо оси X — верный признак фиксированных ширин без адаптивности. - Шаг 2. Проверка viewport-метатегаВ
<head>не оказалось<meta name="viewport">. Браузер на мобильном рендерил страницу как десктоп (шириной ~980px) и целиком масштабировал вниз — отсюда «мелкий неразборчивый десктоп» вместо адаптива. - Шаг 3. Аудит CSS на фиксированные шириныНагуглил через DevTools проблемные правила:
.container { width: 1200px }и карточки тарифов.pricing-card { width: 380px }. Жёсткие пиксели вместо%/fr/flex— прямая причина переполнения. - Шаг 4. Анализ переполнения текстаПрошёлся по блокам с длинными URL и неразрывными строками. Ни
overflow-wrap, ниword-break, ниhyphensне заданы — слово без пробелов растягивало строку за пределы контейнера. - Шаг 5. Разбор перекрытия кнопокКарточки тарифов имели фиксированную
height, а кнопка «Купить» стояла наposition: absoluteвнутри них. При сжатии карточки контент налезал на кнопку, аflex-контейнер карточек не имелflex-wrap— элементы не переносились, а наслаивались. - Шаг 6. Почему молчит бургерJS навешивал слушатель на
document.getElementById('menu-toggle'), а в разметкеmenu-toggleбыл классом, неid— селектор возвращалnull. Дополнительно шапка подгружалась через AJAX, и даже после правки селектора слушатель терялся при перерендере. Плюс медиазапросы шли в порядкеmax-width:1200px → 768px → 480px, и более раннее широкое правило перекрывало позднее узкое по специфичности.
Что исправил
Чинил от корневых причин: viewport, резиновые ширины, перенос текста, поток кнопок и делегирование событий для бургера. Каждое изменение объясняло один-два симптома из диагноза.
1. Viewport-метатег
Без него браузер рендерил десктопную раскладку и масштабировал её целиком. Добавил корректный метатег в <head>.
<head>
<meta charset="UTF-8">
<title>Tariffs</title>
<!-- viewport отсутствует -->
</head>
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, initial-scale=1">
<title>Tariffs</title>
</head>
2. Резиновый контейнер вместо фиксированной ширины
Жёсткие 1200px переполняли любой экран уже при ~1000px. Заменил на max-width + width: 100% с боковыми отступами.
.container {
width: 1200px;
margin: 0 auto;
}
.container {
max-width: 1200px;
width: 100%;
margin: 0 auto;
padding: 0 16px;
box-sizing: border-box;
}
3. Карточки тарифов: flex вместо фиксированной ширины
Карточки 380px не помещались в ряд на мобильном и не переносились. Перевёл их на гибкую основу и включил перенос на родителе.
.pricing-grid {
display: flex;
/* flex-wrap отсутствует — карточки наслаиваются */
}
.pricing-card {
width: 380px;
height: 420px;
position: relative;
}
.pricing-card .buy-btn {
position: absolute;
bottom: 16px;
left: 16px;
}
.pricing-grid {
display: flex;
flex-wrap: wrap;
gap: 16px;
}
.pricing-card {
flex: 1 1 280px;
/* height убран — карточка растёт по содержимому */
display: flex;
flex-direction: column;
gap: 12px;
}
.pricing-card .buy-btn {
/* кнопка в нормальном потоке, не absolute */
align-self: stretch;
margin-top: auto;
}
4. Перенос длинных строк в тексте
Заголовки с длинными URL и неразрывными словами выходили за контейнер. Добавил свойства переноса для заголовков и основного текста.
h1, h2, h3, p, a, .pricing-card {
overflow-wrap: break-word;
word-break: break-word;
}
h1, h2, h3 {
hyphens: auto;
}
5. Бургер-меню: делегирование событий вместо потерянного слушателя
Селектор искал id, которого не было, а шапка перерендеривалась через AJAX — слушатель терялся. Перевёл открытие на делегирование событий и data-атрибуты, чтобы клик работал независимо от порядка подгрузки разметки.
// Селектор возвращает null: в разметке класс, не id
const toggle = document.getElementById('menu-toggle');
toggle.addEventListener('click', () => {
document.querySelector('.nav-links')
.classList.toggle('open');
});
// После AJAX-перерендера шапки toggle уже не в DOM
// — слушатель молча теряется, тап не работает
// Делегирование на document — работает и до, и после AJAX
document.addEventListener('click', (e) => {
const btn = e.target.closest('[data-menu-toggle]');
if (!btn) return;
const menu = document.querySelector('[data-nav-links]');
const isOpen = menu.classList.toggle('open');
btn.setAttribute('aria-expanded', isOpen);
});
В разметке заменил класс/id на data-атрибуты: <button data-menu-toggle aria-expanded="false"> и <nav data-nav-links>.
6. Порядок медиазапросов
Перестроил медиазапросы от узкого к широкому, чтобы поздние специфичные правила не перекрывались ранними общими.
/* Порядок «наоборот»: широкое правило выше узкого */
@media (max-width: 1200px) { /* ... */ }
@media (max-width: 768px) { /* ... */ }
@media (max-width: 480px) { /* ... */ }
/* От узкого к широкому — каждое правило уточняет предыдущее */
@media (max-width: 480px) { /* mobile-first правки */ }
@media (max-width: 768px) { /* планшет */ }
@media (max-width: 1200px) { /* десктоп */ }
Результат
После правок прошёлся по всем брейкпоинтам от 320px до 4K и прогнал PageSpeed Insights. Симптомы ушли на всех ширинах.
Сайт корректно отображается на iPhone и Android от 320px до 4K-десктопа: контейнер резиновый, карточки тарифов переносятся и не наслаиваются, текст переносится внутри блоков, бургер стабильно открывается благодаря делегированию событий. Адаптив теперь принят по умолчанию, а не как отдельный «мобильный» режим.