CSS z-index та контекст накладення
CSS z-index - встановлює порядок накладання позиціонованих елементів по осі Z. Але це порівняння відбувається лише всередині контексту накладання (stacking context), локального контейнера, який ізолює дочірні елементи від решти сторінки.
Теорія
TL;DR
- z-index порівнює лише елементи в одному контексті накладання, а не по всій сторінці
- Дочірній елемент з z-index: 9999 всередині батька з z-index: 1 програє будь-якому елементу з z-index: 2 зовні
- Контексти накладання створюють:
position+ не-autoz-index,opacity < 1,transform,filter,isolation: isolateта інші - Уяви це як ліфтові шахти зі скла: елементи всередині однієї шахти ніколи напряму не перекривають елементи з іншої
- Проблеми зі стеком вирішуй через
isolation: isolate, а не збільшенням чисел
Швидкий приклад
.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 < 1 | opacity: 0.99 |
transform (будь-яке значення) | transform: translateX(0) |
filter (будь-яке значення) | filter: blur(0) |
will-change | will-change: transform |
isolation: isolate | Явно створює контекст |
position: fixed або sticky | Завжди створює контекст |
Дочірній flex/grid елемент з z-index | z-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.
: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 на статичному елементі
.box { z-index: 10; } /* Не працює */z-index ігнорується, якщо position не є relative, absolute, fixed або sticky, або елемент не є flex/grid-дочірнім. Додай position: relative і все запрацює.
Великий z-index замкнений у низькому контексті
.parent { opacity: 0.99; } /* Створює контекст накладання */
.tooltip { position: absolute; z-index: 9999; } /* Застряг всередині parent */opacity запускає новий контекст. Будь-що зовні з z-index: 1 виграє. Перенеси тултіп до кореневого контейнера або підніми z-index батька. Саме цю помилку шукають найдовше в реальних проектах: перевіряєш z-index, збільшуєш, перевіряєш знову — нічого не міняється, бо проблема три рівні вище в DOM.
Від'ємний z-index ховає елемент за фоном
.overlay { position: absolute; z-index: -1; }Від'ємні значення розміщують елемент під фоном батька. Це рідко те, що потрібно. Якщо треба обрізати шар без z-index трюків, використай isolation: isolate на батьку.
Плутанина між flex order і z-index
.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 елементи.
Приклади
Базовий порядок накладання
.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)
// Без порталу модал замкнений у батьківському контексті з 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
<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 — і тултіп знову видно.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.