Контекст наложения. Почему элемент с z-index 9999 все равно перекрывается другим слоем?

Frontend

Вы задали элементу z-index: 999, а он все равно прячется под соседним блоком. Вы добавляете position: relative, играете с числами — а толку ноль. Знакомо? Это мои будни еще пару лет назад. Все изменилось, после того как я понял как работает контекст наложения — правило, которое объясняет, почему порядок слоев работает не так, как вы думаете.

Что такое контекст наложения (Stacking Context)

Контекст наложения — это концепция, с помощью которой браузер определяет порядок отображения элементов вдоль оси Z (перпендикулярно экрану). Она определяет, какой слой окажется выше или ниже при перекрытии.

Контекст наложения на странице чаще всего не один. При этом каждый контекст наложения — это отдельная вселенная со своим порядком слоев. Элементы из разных контекстов не конкурируют друг с другом по z-index.

Представить это можно как стол с нагромождением посуды. Если убрать часть посуды на другой стол (создать новый контекст наложения), она перестает «конкурировать» за вертикальное пространство с посудой с первого стола.

Как браузер раскладывает страницу по оси Z (базовый порядок)

Без учета контекстов наложения браузер рисует слои в таком порядке:

  1. Фон и границы корневого элемента (<html>)
  2. Элементы с отрицательным z-index (в порядке появления)
  3. Блоки без позиционирования (статичные и относительные без z-index)
  4. Элементы с position: relative/absolute/fixed/sticky и z-index: auto (в порядке появления)
  5. Элементы с положительным z-index (в порядке возрастания)

Что создает новый контекст наложения?

Новый контекст наложения создается, когда элемент выходит из потока или получает определенные CSS-свойства.

1. position + z-index

.element {
    position: relative; /* или absolute, fixed, sticky */
    z-index: 5; /* любое число, кроме auto */
}

2. position: fixed или sticky (даже без z-index)

.element {
    position: fixed; /* всегда создает контекст */
}

3. opacity < 1

.element {
    opacity: 0.99; /* любое значение меньше 1 */
}

4. transform, filter, perspective

.element {
    transform: scale(1.1);
    /* или */
    filter: blur(5px);
    /* или */
    perspective: 500px;
}

5. will-change

.element {
    will-change: transform; /* если создает контекст в браузере */
}

6. display: flex / grid + z-index на дочерних элементах

Если у родителя display: flex или display: grid, а у дочернего элемента есть z-index (не auto), то родитель создает контекст наложения.

.parent {
    display: flex;
}
.child {
    z-index: 1; /* родитель создает контекст */
}

7. mix-blend-mode, isolation

.element {
    isolation: isolate; /* принудительно создает контекст */
}

8. container-type

.element {
    container-type: inline-size;
}

9. Другие свойства

  • backdrop-filter
  • mask
  • clip-path (не все браузеры)
  • z-index у position: sticky

Как работает z-index внутри одного контекста

Внутри одного контекста z-index работает как ожидается:

<div class="parent">
    <div class="child1" style="z-index: 10">Я сверху</div>
    <div class="child2" style="z-index: 5">Я снизу</div>
</div>

child1 будет поверх child2, потому что они в одном контексте наложения (у родителя нет свойств, создающих контекст).

Почему z-index не работает (главные ошибки)

Ошибка 1: Элементы в разных контекстах

<div style="position: relative; z-index: 1">
    <div style="position: absolute; z-index: 100">Я внутри контекста A</div>
</div>
<div style="position: relative; z-index: 2">
    <div style="position: absolute; z-index: 1">Я внутри контекста B</div>
</div>

Хотя z-index: 100 больше, чем z-index: 1, второй элемент может оказаться сверху, потому что родитель с z-index: 2 создает новый контекст, и его дочерние элементы соревнуются только внутри этого контекста.

Решение: Убрать z-index у родителя, если он не нужен.

Ошибка 2: Родительский контекст перекрывает всё

<div style="position: relative; z-index: 1">
    <!-- Все дочерние элементы будут под элементами с z-index > 1 на том же уровне -->
    <div style="position: absolute; z-index: 999">Я все равно под вторым блоком</div>
</div>
<div style="position: relative; z-index: 2">
    Я сверху, потому что родительский z-index больше
</div>

Решение: Поднять z-index у родителя, а не у дочернего элемента.

Ошибка 3: position: static (без позиционирования)

/* ❌ z-index не работает без position */
.element {
    z-index: 10;
}

/* ✅ Нужно добавить position (кроме static) */
.element {
    position: relative;
    z-index: 10;
}

Принудительное создание контекста (isolation)

Если вам нужно «отрезать» элемент от родительского контекста, используйте isolation: isolate:

.modal {
    isolation: isolate;
    position: fixed;
    z-index: 1000;
}

Теперь модальное окно создает свой контекст наложения, и его дочерние элементы не будут конфликтовать с остальной страницей.

Итоги

  • Контекст наложения — это изолированное пространство, в котором действует z-index
  • Элементы из разных контекстов не конкурируют по z-index. Порядок определяется родительскими контекстами.
  • Новый контекст создают: position + z-index (кроме auto), position: fixedopacity < 1transformfilterflex/grid с дочерним z-indexisolation: isolate и многие другие.
  • Если z-index не работает — скорее всего, элемент в другом контексте.

Главное, что нужно запомнить: z-index: 9999 не панацея. Он работает только внутри своего контекста. Иногда решение — поднять z-index у родителя или создать новый контекст с помощью isolation: isolate.

Желаю успехов!

Симо Мофин
Симо Мофин

Senior Frontend Developer
Главный по блогу