Блог

Назад
Професійний дебагінг CSS: Контексти накладання, поведінка Flexbox та баги мобільних в'юпортів

10 березня 2026 р.

Професійний дебагінг CSS: Контексти накладання, поведінка Flexbox та баги мобільних в'юпортів

Сучасна веб-верстка майже повністю тримається на CSS Flexbox, Grid та технологіях трансформації елементів. Однак браузерні рушії (Blink, WebKit, Gecko) мають специфічні відмінності та особливості обчислення розмірів, які періодично призводять до виникнення візуальних багів. Елементи можуть раптово зміщуватися, контент виходити за межі екрана, а правила z-index відмовлятися працювати в очікуваному порядку.

Бездумне підбирання CSS-властивостей в інспекторі браузера - це неефективний шлях. У цьому посібнику ми проаналізуємо логіку рендерингу, що стоїть за п'ятьма найпоширенішими проблемами верстки, та розберемо професійні методи їх швидкого усунення.


1. Пошук джерела горизонтальної прокрутки на мобільних (Overflow)

Поява небажаного горизонтального скролу на мобільному макеті є однією з найпоширеніших проблем адаптивної верстки. Це відбувається, коли сумарна ширина дочірніх елементів перевищує ширину в'юпорту. Оскільки шукати винуватця вручну у великому дереві DOM складно, ми можемо автоматизувати цей процес за допомогою консолі браузера.

Виявлення елементів переповнення через консоль

Відкрийте інструменти розробника, перейдіть на вкладку Console та запустіть наступний JavaScript-код:

// Знайти всі елементи, які виходять за межі ширини тіла документа
document.querySelectorAll('*').forEach(el => {
  const rect = el.getBoundingClientRect();
  if (rect.right > window.innerWidth) {
    console.log('Елемент переповнення:', el, `Права координата: ${rect.right}px`);
  }
});

Цей скрипт обходить усі вузли DOM-дерева та виводить у консоль ті елементи, які виступають за межі екрана. При наведенні курсору на виведені вузли в консолі браузер підсвітить їх безпосередньо на сторінці.

Типові рішення проблеми:

  1. Глобальне скидання Box-Sizing: Переконайтеся, що ваш файл глобальних стилів містить це налаштування. Без нього внутрішні відступи та рамки додаються до заданої ширини елементів, виштовхуючи їх за межі контейнерів:
    *, *::before, *::after {
      box-sizing: border-box;
    }
    
  2. Адаптивні зображення та медіа-файли: Картинки та відео без обмеження максимальної ширини рендеритимуться у своїй фізичній роздільній здатності, що призведе до виходу за межі екранів смартфонів:
    img, video {
      max-width: 100%;
      height: auto;
    }
    
  3. Пріоритет max-width над width: Уникайте використання жорстко заданої ширини в пікселях для макетних контейнерів. Використовуйте гнучкі відсотки у поєднанні з обмеженням максимальної ширини:
    .container {
      width: 100%;
      max-width: 1200px;
      margin: 0 auto;
    }
    

2. Розрахунок розмірів у Flexbox: обмеження min-width: auto

Поширена проблема з Flexbox виникає, коли флекс-елемент містить довге слово, нерозривну URL-адресу або великий блок коду. Замість того щоб перенести текст на новий рядок або зменшитися, елемент розширюється і виходить за межі батьківського контейнера, ламаючи всю сітку.

Чому це відбувається?

Згідно зі специфікацією W3C Flexbox, мінімальна ширина флекс-елемента за замовчуванням дорівнює не 0, а min-width: auto. Браузерні двигуни розраховують цей розмір на основі мінімального розміру контенту дочірніх елементів. Якщо дочірній елемент містить рядок тексту, який не можна перенести (наприклад, довге посилання), флекс-елемент відмовиться стискатися менше довжини цього рядка, ігноруючи ваші правила розподілу простору.

       Переповнення Flexbox через довгий текст
       
+--------------------------------------------------------+
| Батьківський блок Flex (ширина: 400px)                 |
+------------------------------------+-------------------+
| Елемент A (ширина: 100px)          | Елемент B         |
|                                    | (текст вилазить)  |
|                                    | "https://example.com/very/long/url..."
+------------------------------------+-------------------+
                                      <-- Переповнення тут!

Вирішення:

Щоб змусити флекс-елемент ігнорувати мінімальну ширину контенту та стискатися під розмір контейнера, встановіть для нього явне обмеження мінімальної ширини в 0:

.flex-child-container {
  min-width: 0; /* Дозволяє елементу стискатися менше за його контент */
  overflow: hidden; /* Або text-overflow: ellipsis для обрізання тексту */
}

Для вертикальних макетів діють такі ж правила: елементи мають значення за замовчуванням min-height: auto, яке можна скинути за допомогою min-height: 0.


3. CSS Grid: Баг розрахунку колонок у minmax()

CSS Grid надає неймовірні можливості контролю сітки, але розробники часто стикаються з розширенням колонок при інтеграції динамічних текстових елементів.

Наприклад, розглянемо таку структуру сітки:

.grid-container {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  gap: 20px;
}

Якщо один із елементів сітки містить довгий блок коду в тегу <pre> або широку таблицю, колонка сітки розшириться далеко за межі пропорції 1fr, виштовхнувши сусідні колонки за рамки екрана.

Логіка обчислення Grid-треків

Одиниця виміру 1fr розподіляє вільний простір, що залишився. Але при розрахунках браузер спершу перевіряє мінімальний розмір контенту. Якщо цей мінімальний розмір більше за 300px, V8 вибере саме його, щоб уникнути обрізання інформації.

Вирішення:

Ви можете контролювати поведінку колонок сітки, додавши обмеження мінімальної ширини для елементів:

.grid-item {
  min-width: 0; /* Запобігає неконтрольованому розширенню картки */
}

Щоб побачити межі колонок, відкрийте Chrome DevTools, знайдіть контейнер сітки та натисніть на позначку Grid в панелі елементів. Це ввімкне наочну візуалізацію сітки безпосередньо на сторінці.


4. Проблеми з Z-Index: Контексти накладання (Stacking Contexts)

Типова ситуація з z-index: ви задаєте для спливаючого вікна z-index: 99999, але воно все одно ховається під шапкою сайту, яка має z-index: 2.

Робота контекстів накладання

Контекст накладання (Stacking Context) - це тривимірна ієрархія шарів, яку формує браузер під час рендерингу. Як тільки елемент створює новий контекст накладання, всі його дочірні елементи рендеряться виключно всередині цього локального шару. Вони не можуть порівнюватися за рівнем z-index з елементами з інших шарів.

Новий контекст накладання створюється у таких випадках:

  • Елемент має position: relative або absolute разом із z-index, який не дорівнює auto.
  • Встановлено position: fixed або sticky.
  • Значення opacity менше 1.
  • Використовуються CSS-властивості transform, filter, perspective або clip-path.
  • Задано властивість will-change з будь-яким значенням, що впливає на рендеринг.
                    Ієрархія контекстів накладання
                    
Базовий контекст (Viewport)
  ├── Контекст A (z-index: 1)
  │     └── Елемент A1 (z-index: 9999)  <-- Заблокований всередині Контексту A!
  │
  └── Контекст B (z-index: 2)
        └── Елемент B1 (z-index: 1)     <-- Відображається поверх A1!

Оскільки Контекст B знаходиться вище за Контекст A, всі дочірні об'єкти Контексту B відображатимуться поверх об'єктів Контексту A, незалежно від того, які локальні значення z-index ви задасте для елементів на кшталт A1.

Баг 3D-трансформацій у Safari (WebKit)

У браузері Safari (WebKit) є додаткова особливість: елементи з підтримкою апаратного прискорення (transform: translate3d(...)) можуть пробивати стандартні контексти накладання. Щоб вирішити цю проблему, додайте transform-style: flat до батьківських контейнерів або примусово оновіть шар для потрібного елемента:

.target-element {
  transform: translate3d(0, 0, 0);
  will-change: transform; /* Змушує Safari перерахувати індекси шарів */
}

5. Мобільні в'юпорти: вирішення проблеми 100vh в iOS

При створенні інтерфейсів на весь екран використання height: 100vh на мобільних пристроях часто призводить до неприємного багу. Нижня частина вашого екрана ховається під динамічною навігаційною панеллю браузера Safari чи Chrome на iOS, закриваючи важливі кнопки.

Чому так відбувається?

Специфікація W3C визначає висоту в'юпорту (vh) як статичну величину. Розробники мобільних браузерів вирішили зробити 100vh рівним висоті екрана при повністю прихованій панелі навігації. Коли користувач заходить на сайт і панель відображається, вона просто перекриває нижню частину сторінки.

       vh проти svh/dvh на мобільних пристроях
       
+-------------------------------+  <-- Верхня межа екрана
|                               |
|                               |
|        Контент сторінки       |
|                               |
|                               |
+-------------------------------+  <-- 100svh (Нижня межа при відкритій панелі)
|  Панель навігації браузера    |
+-------------------------------+  <-- 100vh / 100lvh (Межа при прихованій панелі)

Вирішення: Сучасні одиниці виміру в'юпорту

Специфікація CSS CSS Values and Units Module Level 4 додала нові одиниці виміру для розв'язання цієї проблеми:

  • svh (Small Viewport Height): Висота в'юпорту при відкритих панелях браузера (найбезпечніший вибір для інтерфейсів).
  • lvh (Large Viewport Height): Висота при повністю прихованих панелях.
  • dvh (Dynamic Viewport Height): Динамічна висота, яка змінюється в реальному часі, коли користувач гортає сторінку і панель приховується чи з'являється.

Для підтримки старих пристроїв використовуйте такий каскад стилів:

.full-screen-container {
  height: 100vh; /* Для старих браузерів */
  height: 100dvh; /* Динамічне підлаштування для сучасних мобільних браузерів */
}

6. Субпіксельне округлення та щілини в 1px

Чи стикалися ви з тим, що при розміщенні трьох колонок шириною рівно 33.33% третя колонка несподівано переносилася на новий рядок, або між сусідніми кольоровими блоками з'являлася тонка напівпрозора лінія? Цей баг викликаний субпіксельними округленнями.

Як браузери рахують пікселі

Фізичний екран складається з цілих пікселів. Але координати макету в CSS часто є дробовими числами (наприклад, 350.67px). При виведенні цих значень на екран браузер змушений округлювати їх до цілих значень.

Різні браузерні рушії роблять це по-різному:

  • Деякі просто відкидають дробову частину (floor), через що сума трьох колонок може виявитися на 1px меншою за батьківський контейнер, залишаючи щілину.
  • Інші округлюють за математичними правилами в більшу сторону, що може дати сумарну ширину трьох колонок на 1px більшу за ширину батька. Це змушує останній елемент переноситися на наступний рядок.
  • Ця помилка найчастіше з'являється при зміні масштабу сторінки користувачем (наприклад, до 110%) або на екранах із високою щільністю пікселів (Retina), де коефіцієнт масштабування є дробовим.

Вирішення:

  1. Використовуйте Flexbox або Grid замість Float-верстки: Сучасні модулі розмітки розроблені з урахуванням субпіксельних похибок і автоматично розподіляють зайві пікселі.
  2. Невеликий нахлест для кольорових блоків: Якщо ви маєте суміжні кольорові блоки і між ними видно мікрощілини, додайте від'ємний маргін:
    .adjacent-block {
      margin-right: -1px; /* Створює мінімальний нахлест для маскування щілини */
    }
    
  3. Резервний субпіксельний буфер у розрахунках: Якщо ви ділите ширину через calc(), відніміть мікроскопічну частку пікселя, щоб уникнути перенесення:
    .column {
      width: calc(33.33% - 0.1px);
    }
    

Розуміння логіки роботи браузерних рушіїв при обчисленні розмірів та шарів дозволить вам швидко знаходити джерела багів та створювати надійну і стабільну верстку.