Skip to main content

CSS z-index та контекст накладення

CSS z-index - встановлює порядок накладання позиціонованих елементів по осі Z. Але це порівняння відбувається лише всередині контексту накладання (stacking context), локального контейнера, який ізолює дочірні елементи від решти сторінки.

Теорія

TL;DR

  • z-index порівнює лише елементи в одному контексті накладання, а не по всій сторінці
  • Дочірній елемент з z-index: 9999 всередині батька з z-index: 1 програє будь-якому елементу з z-index: 2 зовні
  • Контексти накладання створюють: position + не-auto z-index, opacity < 1, transform, filter, isolation: isolate та інші
  • Уяви це як ліфтові шахти зі скла: елементи всередині однієї шахти ніколи напряму не перекривають елементи з іншої
  • Проблеми зі стеком вирішуй через isolation: isolate, а не збільшенням чисел

Швидкий приклад

css
.parent { position: relative; z-index: 5; } /* Створює контекст накладання */ .child { position: absolute; z-index: 100; } /* Замкнений всередині parent */ .other { position: absolute; z-index: 6; } /* Виграє — порівнюється з parent z=5, а не child z=100 */

У child є z-index: 100, але він все одно під .other. Браузер порівнює .parent (z=5) з .other (z=6). Значення child у цьому змаганні не враховується. Воно конкурує лише всередині .parent.

Як працюють контексти накладання

Коли браузер малює сторінку, він сортує елементи за z-order кореневого елемента їхнього контексту накладання, а не за кожним індивідуальним z-index. Кожен контекст малюється як єдина плоска група. Браузер не заглядає всередину. Контекст з z-index: 1 повністю відрисовується перед контекстом з z-index: 2, незалежно від того, які значення є всередині.

Кореневий <html> починає перший контекст накладання. Властивості на кшталт position: relative; z-index: 1 рекурсивно запускають нові. Chrome (рушій Blink) відстежує кожен контекст як об'єкт PaintLayer для GPU-композитингу.

Що створює контекст накладання

Властивість / УмоваПриклад
Кореневий елемент<html>
position + z-index (не auto)position: relative; z-index: 1
opacity < 1opacity: 0.99
transform (будь-яке значення)transform: translateX(0)
filter (будь-яке значення)filter: blur(0)
will-changewill-change: transform
isolation: isolateЯвно створює контекст
position: fixed або stickyЗавжди створює контекст
Дочірній flex/grid елемент з z-indexz-index: 1 на flex-дочірньому елементі

Найчастіший сюрприз — opacity: 0.99. Додав його для плавного fade-ефекту, і він автоматично створює контекст накладання. Тултіп всередині раптово ховається за сайдбаром без жодної очевидної причини.

Коли і як використовувати

  • Модальне вікно або drawer: position: fixed; z-index: 1000 на рівні кореня. У React використовуй ReactDOM.createPortal(modal, document.body), щоб вийти з будь-якого батьківського контексту.
  • Вкладений dropdown: застосуй isolation: isolate до батьківського компонента, щоб внутрішні z-index значення не виходили назовні.
  • Картки, що перекриваються: position: relative; z-index: auto на сусідніх елементах — без створення нових контекстів.
  • Уникай великих чисел: замість z-index: 99999 оголоси шкалу через CSS custom properties.
css
:root { --z-dropdown: 100; --z-modal: 400; --z-tooltip: 600; } .modal { position: fixed; z-index: var(--z-modal); } .tooltip { position: fixed; z-index: var(--z-tooltip); }

Типові помилки

z-index на статичному елементі

css
.box { z-index: 10; } /* Не працює */

z-index ігнорується, якщо position не є relative, absolute, fixed або sticky, або елемент не є flex/grid-дочірнім. Додай position: relative і все запрацює.

Великий z-index замкнений у низькому контексті

css
.parent { opacity: 0.99; } /* Створює контекст накладання */ .tooltip { position: absolute; z-index: 9999; } /* Застряг всередині parent */

opacity запускає новий контекст. Будь-що зовні з z-index: 1 виграє. Перенеси тултіп до кореневого контейнера або підніми z-index батька. Саме цю помилку шукають найдовше в реальних проектах: перевіряєш z-index, збільшуєш, перевіряєш знову — нічого не міняється, бо проблема три рівні вище в DOM.

Від'ємний z-index ховає елемент за фоном

css
.overlay { position: absolute; z-index: -1; }

Від'ємні значення розміщують елемент під фоном батька. Це рідко те, що потрібно. Якщо треба обрізати шар без z-index трюків, використай isolation: isolate на батьку.

Плутанина між flex order і z-index

css
.container { display: flex; } .item1 { order: 2; z-index: 10; position: relative; } .item2 { order: 1; z-index: 1; position: relative; transform: translateX(0); }

.item2 опиняється зверху попри z-index: 1. transform виводить його в окремий шар композитингу. Багато розробників очікують, що DOM-порядок і візуальний порядок збігаються. Вони не збігаються, коли є compositing layers.

Реальне використання

  • React Portal: ReactDOM.createPortal(modal, document.body) розміщує модал у кореневому контексті накладання, без пасток батьківських елементів
  • TailwindCSS: утиліта z-50 на fixed елементах у Next.js застосунках працює за тим самим принципом
  • Bootstrap: .modal-backdrop використовує z-index: 1040 всередині контексту, створеного через opacity: 0.5 на елементі фону
  • Сучасна альтернатива: псевдоелемент ::backdrop і Popover API обробляють оверлеї без ручного z-index стекінгу в браузерах, що їх підтримують

Питання на співбесіді

Q: У чому різниця між z-index: auto і z-index: 0?
A: auto означає, що елемент бере участь у батьківському контексті накладання без створення нового. 0 створює новий контекст накладання на нульовому рівні, що змінює поведінку дочірніх елементів.

Q: Мій модал має z-index: 9999, але ховається за навбаром. Чому?
A: Навбар або один з його предків має transform чи filter, що створює вищий контекст накладання. Перевіряй computed styles кожного предка аж до <body> і шукай ці властивості.

Q: Як will-change: transform впливає на накладання?
A: Він виводить елемент на GPU-шар композитингу і створює контекст накладання ще до початку анімації. Це запобігає jank під час переходів, але може стати несподіванкою, якщо забудеш, що контекст вже створено заздалегідь.

Q: Чи є випадок, коли z-index: auto і z-index: 0 поводяться однаково?
A: Так, коли елемент не є позиціонованим і не є flex/grid-дочірнім. У такому випадку обидва значення нічого не роблять. Різниця з'являється лише там, де елемент міг би створити контекст накладання.

Q: (Senior) Чи працює z-index через межу Shadow DOM?
A: Ні. Shadow root створює контекст накладання, і z-index не пробивається через тіньову межу. Для крос-граничного накладання в Chrome 89+ можна використовувати ::part() або slotted елементи.

Приклади

Базовий порядок накладання

css
.card-a { position: relative; z-index: 1; background: lightblue; } .card-b { position: relative; z-index: 2; /* Відображається над card-a */ background: coral; }

Два сусідніх елементи з position: relative, обидва в кореневому контексті накладання. Їхні z-index значення порівнюються напряму. Вище значення виграє. Це простий випадок, який працює саме так, як очікується.

Модал над картками дашборду (React Portal)

jsx
// Без порталу модал замкнений у батьківському контексті з z-index: 1 function Dashboard() { return ( <div style={{ position: 'relative', zIndex: 1 }}> <Card style={{ position: 'relative', zIndex: 10 }}> Картка дашборду </Card> {ReactDOM.createPortal( <div style={{ position: 'fixed', inset: 0, zIndex: 1000 }}> Вміст модалу {/* Тепер у кореневому контексті, перемагає все */} </div>, document.body )} </div> ); }

Без порталу модал знаходиться всередині div з z-index: 1. Навіть з z-index: 1000 він не переможе сусідній елемент з z-index: 2 зовні. Портал переміщує його до document.body, розміщуючи в кореневому контексті, де порівняння відбувається напряму.

Пастка transform

html
<div class="sidebar" style="position: relative; z-index: 10; transform: translateX(0);"> <!-- transform створює контекст накладання --> <div class="tooltip" style="position: absolute; z-index: 9999;"> Цей тултіп у пастці всередині sidebar </div> </div> <div class="header" style="position: relative; z-index: 11;"> Header перемагає тултіп попри його z=9999 </div>

transform сайдбара створює контекст накладання. z-index: 9999 тултіпа конкурує лише всередині сайдбара. Header порівнюється з сайдбаром (z=10), а не з тултіпом (z=9999), і виграє з z=11. Приберіть transform з сайдбара або збільште його z-index — і тултіп знову видно.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?