У редакторі ICanUp я відкрила Media Explorer із Content Block, щоб вибрати зображення, і побачила дивну картину: модальне вікно вже відкрите, але частина елементів редактора все одно може малюватися поверх нього.
Проблема виглядала як звичайний конфлікт z-index.
Перша реакція в такій ситуації дуже передбачувана: просто поставити модальному вікну ще більше число.
Але CSS працює не як один глобальний список шарів. І саме це робить такі баги значно цікавішими.

Симптом виглядав як неправильний z-index
Під час відкритого Media Explorer верхнім інтерактивним шаром має бути саме він.
Під ним не повинні з’являтися:
- контейнери Content Blocks;
- заголовки та drag handles блоків;
- кнопки дій;
- sticky controls редактора;
- dropdowns і toolbars сторінки позаду;
- будь-які інші елементи, що належать фоновому редактору.
Коли хоча б один із цих елементів опиняється над модальним вікном, проблема вже не косметична.
Порушується сама ієрархія взаємодії: користувач бачить overlay, але фон візуально продовжує конкурувати з ним.
Чому z-index не є глобальним рейтингом елементів
Найважливіше правило тут таке: z-index порівнюється всередині конкретної системи накладання.
Якщо два елементи належать до різних stacking contexts, браузер спочатку порівнює самі контексти, а вже потім їхніх дітей.
<div class="editor"> <div class="content-block"> <button class="block-action">Edit</button> </div></div> <div class="overlay-host"> <div class="media-explorer"> ... </div></div>Якщо .editor і .overlay-host опинилися в різних stacking contexts, величезний z-index на .media-explorer може залишатися замкненим усередині контексту свого батька.
Уявімо два окремі контексти накладання
.editor { position: relative; z-index: 20;} .overlay-host { position: relative; z-index: 10;} .media-explorer { position: fixed; z-index: 999999;}Інтуїтивно здається, що 999999 має перемогти 20.
Але якщо .overlay-host вже сформував власний stacking context із рівнем 10, його дочірній Media Explorer не може просто перестрибнути контекст .editor із рівнем 20.
root stacking context ├── editor context z-index: 20│ └── Content Block│└── overlay-host context z-index: 10 └── Media Explorer z-index: 999999Дочірній елемент може бути найвищим у своєму stacking context і все одно залишатися нижче сусіднього контексту.
Stacking context створює не тільки z-index
Через це під час діагностики недостатньо шукати лише z-index.
Новий stacking context можуть створювати різні CSS-властивості.
Властивість або умова | Чому важлива |
|---|---|
| Класичний спосіб створити окремий контекст |
| Може створити власний stacking context |
| Також бере участь у власній системі накладання |
| Навіть transform без візуально помітного переміщення може створити контекст |
| Ізолює нащадків у новому контексті |
| Також може створити stacking context |
| Створює контекст навмисно |
transform особливо легко пропустити
У складній адмінці transform може з’явитися не через анімацію всього модального вікна.
Його можуть додати drag & drop, transition, component library або невеликий ефект:
.editor-section { transform: translateZ(0);}На екрані такий елемент може виглядати абсолютно звичайно, але для браузера він уже створив нову межу накладання.
Тому під час пошуку причини я дивлюся не тільки на сам Media Explorer, а й на весь ланцюжок його предків.
Потрібно перевіряти весь ancestor chain
Media Explorer | vModal wrapper | vEditor component | vLayout container | vAdmin rootБудь-який елемент у цьому ланцюжку може створити stacking context і обмежити всі дочірні z-index.
Те саме потрібно перевірити для елемента, який несподівано опинився зверху.
DevTools корисніший за випадкове збільшення числа
Замість циклу 1000 -> 10000 -> 999999 я хочу спочатку відповісти на кілька конкретних питань.
- Який елемент фактично перекриває overlay?
- До якого stacking context він належить?
- До якого stacking context належить Media Explorer?
- Який ancestor створив кожен із цих контекстів?
- Які computed styles створюють цю межу?
- Чи справді overlay знаходиться достатньо високо в DOM?
У браузерних DevTools для цього корисно перевіряти computed values для position, z-index, transform, opacity, filter та isolation.
position: fixed теж не завжди означає viewport
Є ще одна пастка.
Зазвичай position: fixed асоціюється з viewport, тому здається природним використовувати його для модального вікна.
Але певні властивості на ancestor, зокрема transform, можуть змінити containing block для fixed-нащадка.
.admin-content { transform: translate3d(0, 0, 0);} .media-explorer { position: fixed; inset: 0;}Тому просто побачити position: fixed недостатньо. Потрібно дивитися, де саме елемент знаходиться в DOM і які властивості мають його предки.
Backdrop і dialog повинні належати одному шару
Media Explorer складається не лише з вікна.
Є щонайменше два елементи:
Overlay├── Backdrop└── DialogЯкщо backdrop і dialog живуть у різних випадкових рівнях, можна отримати ще дивнішу поведінку: фон затемнений, але кнопка Content Block знову з’являється поверх затемнення або навіть поверх самого dialog.
Для мене вони повинні утворювати один узгоджений top-level overlay stack.
Глобальний overlay краще не вкладати глибоко в редактор
Один із найнадійніших архітектурних підходів для складних модальних компонентів у Vue полягає в тому, щоб рендерити їх ближче до кореня застосунку, а не залишати всередині компонента, який їх відкрив.
<body> <div id="app"> <!-- editor --> </div> <div id="overlay-root"> <!-- global overlays --> </div></body>У Vue для цього часто підходить Teleport.
<Teleport to="body"> <MediaExplorer v-if="isOpen" /></Teleport>Я не хочу розкидати z-index по кожному Content Block
Найгірший напрям після такого бага виглядає приблизно так:
.block-card { z-index: 20;} .block-actions { z-index: 100;} .editor-toolbar { z-index: 500;} .media-explorer { z-index: 999999;}Так система поступово перетворюється на набір чисел, значення яких уже ніхто не розуміє.
Наступний dropdown отримує 1000000, наступне модальне вікно ще більше число, а початкова причина залишається.
Замість цього потрібен контракт шарів
Я хочу, щоб рівні інтерфейсу мали зрозуміле призначення.
:root { --z-base: 0; --z-sticky: 100; --z-dropdown: 200; --z-overlay: 1000; --z-modal: 1100; --z-toast: 1200;}Конкретні числа тут менш важливі за сам контракт.
Важливо, щоб усі компоненти розуміли:
- звичайний контент живе на базовому рівні;
- sticky controls можуть бути вище контенту, але нижче глобального overlay;
- dropdown має визначений рівень;
- modal і backdrop мають глобальний верхній рівень;
- toast може бути окремим найвищим службовим шаром.
Media Explorer має бути top interactive layer
Для Media Explorer контракт особливо простий.
Поки він відкритий, фон не повинен конкурувати з ним ні візуально, ні інтерактивно.
Media Explorer open Interactive:- Media Explorer dialog- Explorer folders- Explorer files- Explorer internal dropdowns- selection controls Not interactive:- Content Blocks- editor actions- sticky page controls- page dropdowns behind the overlaypointer-events не виправляє неправильний stacking order
Можна тимчасово заборонити взаємодію з фоном через pointer-events, але це вирішує іншу задачу.
.editor[aria-hidden="true"] { pointer-events: none;}Якщо елемент візуально продовжує малюватися над modal, pointer-events: none не виправляє layering.
Він лише робить неправильний шар неклікабельним.
Overflow може створити інший клас проблем
Не кожен візуальний баг модального вікна є stacking context.
Іноді проблема виглядає схоже, але overlay просто обрізається ancestor із overflow.
.editor-panel { overflow: hidden;}Тому під час діагностики я окремо перевіряю дві речі:
Симптом | Ймовірний напрям перевірки |
|---|---|
Інший елемент малюється поверх modal | stacking context / z-index |
Частина modal фізично обрізана | overflow / containing block |
Fixed overlay рухається разом із контейнером | transform / containing block |
Фон клікається через overlay | pointer-events / backdrop coverage |
Баг потрібно перевірити не тільки в Post editor
Media Explorer у ICanUp є повторно використовуваним компонентом.
Тому виправлення лише одного місця, де він відкрився з Post editor, було б занадто вузьким.
Якщо причина лежить у самому overlay contract, потрібно перевірити всі редактори, які використовують цей компонент.
- Post editor;
- Page editor;
- News editor;
- інші місця з Content Blocks;
- мобільний і планшетний layout.
Внутрішні dropdowns Media Explorer теж повинні залишитися правильними
Після підняття самого Media Explorer не можна випадково зламати його внутрішні шари.
Наприклад, dropdown усередині dialog має бути вище його контенту, але все одно належати до загальної модальної ієрархії.
Global modal layer└── Media Explorer ├── backdrop ├── dialog └── internal dropdownЩо я перевіряю після виправлення
- Жоден Content Block не малюється поверх Media Explorer.
- Drag handles не виходять над backdrop.
- Sticky controls залишаються під overlay.
- Backdrop закриває весь потрібний viewport адмінки.
- Під час прокрутки порядок шарів не змінюється.
- Відкриття папок не викликає layer jumping.
- Preview і selection controls працюють поверх dialog.
- Dropdowns усередині Media Explorer не ховаються за самим dialog.
- Закриття overlay повертає звичайну взаємодію з редактором.
- Той самий контракт працює в інших редакторах.
- Мобільний layout не створює окрему проблему накладання.
Автоматичний тест тут не замінить візуальну перевірку
CSS layering складно повністю довести звичайним unit test.
Але тест усе одно може захистити важливу частину контракту: Media Explorer рендериться в очікуваному overlay host, modal отримує правильний layer token, а редактор не створює випадковий локальний modal wrapper.
Після цього я все одно перевіряю реальне відображення в браузері.
Що я не роблю після такого бага
- Не збільшую z-index навмання, не перевіривши stacking contexts.
- Не вважаю z-index глобальним рейтингом усіх елементів сторінки.
- Не перевіряю тільки сам modal і не ігнорую його ancestors.
- Не забуваю про transform, opacity, filter та isolation.
- Не розкидаю випадкові z-index по кожному Content Block.
- Не використовую pointer-events як заміну правильному layering.
- Не виправляю reused Media Explorer лише в одному editor.
- Не забуваю окремо перевірити backdrop, dialog та внутрішні dropdowns.
Мій короткий checklist для stacking context bugs
- Знайти конкретний елемент, який перекриває overlay.
- Визначити stacking context обох елементів.
- Перевірити ancestor chain.
- Переглянути position і z-index.
- Перевірити transform.
- Перевірити opacity.
- Перевірити filter.
- Перевірити isolation.
- Перевірити overflow і containing block.
- Для глобального overlay розглянути top-level host або Vue Teleport.
- Використовувати зрозумілий layer contract замість випадкових чисел.
- Перевірити всі місця повторного використання компонента.
Великий z-index не робить елемент глобально верхнім. Спочатку потрібно зрозуміти, у якому stacking context він взагалі живе.
Висновок
Баг із Media Explorer у ICanUp виглядав як проста задача про z-index, але насправді це задача про структуру CSS-шарів.
Якщо overlay знаходиться всередині локального stacking context, величезне число на самому dialog не гарантує, що він опиниться вище за сусідній контекст редактора.
Тому я дивлюся не тільки на z-index, а на весь ancestor chain, transform, position, opacity, filter, isolation, overflow та фактичне місце overlay у DOM.
Для повторно використовуваного Media Explorer правильнішим напрямом є не гонка чисел, а чіткий top-level overlay contract, де backdrop і dialog гарантовано знаходяться вище за весь редактор.
Для мене головний урок тут простий: коли z-index не працює так, як очікується, потрібно перестати збільшувати число і почати шукати межу stacking context.

