HTML & CSS

Позиціонування в CSS. Z-index. Stacking Context

Глибокий розгляд position static, relative, absolute, fixed, sticky. Z-index та контекст накладання (stacking context). Практика: tooltip, modal overlay, sticky header, dropdown menu.

Позиціонування в CSS. Z-index. Stacking Context

Чому z-index: 9999 іноді не допомагає?

Уявіть: ви додаєте модальне вікно поверх всього сайту, ставите z-index: 9999, але воно все одно перекривається якимось дрібним елементом із z-index: 10. Ви збільшуєте до z-index: 99999 — і знову нічого. Це не баг браузера. Це stacking context (контекст накладання) — одна з найменш зрозумілих, але вкрай важливих концепцій CSS.

Щоб зрозуміти stacking context, потрібно спочатку розібратися з позиціонуванням — бо саме position є воротами до всього іншого.

У попередній статті ми навчились будувати макети через Grid. Тепер з'ясуємо, як виривати елементи з нормального потоку документа і розміщувати їх довільно на сторінці.


Нормальний потік документа

Перш ніж говорити про позиціонування, важливо зрозуміти, від чого ми відходимо.

Нормальний потік (normal flow) — це стандартна поведінка браузера при відображенні елементів. Блокові елементи (div, p, h1) займають весь рядок і розміщуються одне під одним. Рядкові елементи (span, a, strong) течуть у рядках зліва направо.

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


Властивість position

position — ключова властивість CSS для управління розміщенням елементів. Вона визначає, в якій системі координат позиціонується елемент.

.element {
    position: static; /* за замовчуванням */
    position: relative;
    position: absolute;
    position: fixed;
    position: sticky;
}

Разом із position використовуються зміщувальні властивості: top, right, bottom, left. Вони задають відстань від відповідного краю.

Loading diagram...
graph LR
    A["position: static\n(нормальний потік)"] --> B["position: relative\n(відносно свого місця)"]
    B --> C["position: absolute\n(відносно позиціонованого предка)"]
    B --> D["position: fixed\n(відносно viewport)"]
    B --> E["position: sticky\n(hybrid: relative + fixed)"]
    style A fill:#94a3b8,color:#fff
    style B fill:#6366f1,color:#fff
    style C fill:#f59e0b,color:#fff
    style D fill:#10b981,color:#fff
    style E fill:#ec4899,color:#fff

position: static — за замовчуванням

static — це початкове значення. Елемент перебуває у нормальному потоці, властивості top, right, bottom, left та z-index на нього не діють.

.element {
    position: static; /* Це те саме, що не вказувати position взагалі */
    top: 20px; /* Ігнорується! */
    z-index: 100; /* Ігнорується! */
}
z-index не працює без position (або без display: flex/grid на елементі). Це одна з найпоширеніших причин, чому z-index "не діє". Якщо ви хочете підняти елемент через z-index, він повинен мати position: relative, absolute, fixed або sticky.

position: relative — зміщення без виходу з потоку

relative — перший крок до позиціонування. Елемент залишається у нормальному потоці (займає своє місце), але ви можете зміщувати його відносно цього місця через top, right, bottom, left.

Ключова особливість: місце, яке елемент займав у потоці, зберігається — сусідні елементи не "заповзуть" на його колишнє місце.

.shifted {
    position: relative;
    top: 20px; /* Зсунути вниз на 20px відносно нормальної позиції */
    left: 30px; /* Зсунути вправо на 30px */
}
Preview
×
🔒localhost:3000

Зверніть: Box 3 залишається на своєму місці, ніби Box 2 нікуди не рухався. Порожнє місце Box 2 зберігається у потоці. Це фундаментальна різниця між relative і absolute.

Головна роль relative — бути координатним предком для absolute

Саме по собі зміщення relative-елементів використовується рідко. Головна роль position: relativeпозначити елемент як систему відліку для дочірнього position: absolute. Детальніше — в наступному розділі.


position: absolute — абсолютне позиціонування

absolute — це повний вихід з нормального потоку. Елемент більше не займає місця серед сусідів; він позиціонується відносно найближчого позиціонованого предка (containing block).

Позиціонований предок — це будь-який батьківський елемент, у якого position відрізняється від static (тобто relative, absolute, fixed, sticky).

.parent {
    position: relative; /* 🔑 Задає систему відліку для дочірнього absolute */
}

.child {
    position: absolute;
    top: 10px; /* 10px від верхнього краю .parent */
    right: 10px; /* 10px від правого краю .parent */
}
Preview
×
🔒localhost:3000

Що відбувається, якщо позиціонованого предка немає?

Якщо жоден батьківський елемент не має position відмінного від static, absolute-елемент позиціонується відносно <html> (кореневого елемента сторінки). Це часто небажана поведінка, тому правило:

Якщо ставите position: absolute дочірньому елементу — завжди ставте position: relative батьківському.

Патерн badge: розміщення значка (кількість сповіщень, "новинка", онлайн-статус) у кутку іконки або картки — класичний кейс absolute всередині relative:
.card {
    position: relative;
}
.badge {
    position: absolute;
    top: -6px;
    right: -6px;
    /* ... */
}

Практика: спливаючий tooltip

Preview
×
🔒localhost:3000

Розберемо ключові моменти реалізації tooltip:

  • .tooltip-trigger { position: relative } — батько є системою відліку.
  • .tooltip-text { position: absolute; bottom: calc(100% + 8px) }100% відповідає висоті кнопки, тобто tooltip з'являється рівно вище кнопки з відступом 8px.
  • left: 50%; transform: translateX(-50%) — класичний трюк для горизонтального центрування абсолютного елемента невідомої ширини.
  • opacity: 0opacity: 1 при :hover — плавна поява через transition.
  • ::after — псевдоелемент для "хвостика" через трюк з прозорими рамками.

position: fixed — відносно вікна браузера

fixed — схоже на absolute, але система відліку — viewport (видима область браузера), а не позиціонований предок. Елемент не прокручується разом зі сторінкою — залишається на місці при скролі.

.sticky-header {
    position: fixed;
    top: 0; /* Притиснутий до верху viewport */
    left: 0;
    width: 100%; /* Займає всю ширину */
    z-index: 1000; /* Поверх інших елементів */
}

.back-to-top {
    position: fixed;
    bottom: 2rem;
    right: 2rem;
}
Preview
×
🔒localhost:3000

Важливий підводний камінь fixed

transform ламає position: fixed! Якщо будь-який предок елемента з position: fixed має властивість transform, filter, perspective або will-change: transform, то fixed-елемент позиціонується відносно цього предка, а не відносно viewport. Це поведінка за специфікацією, але часто несподівана. Вирішення: переміщуйте fixed-елементи вище в DOM, щоб вони не були нащадками трансформованих елементів.

position: sticky — "липкий" елемент

sticky — це гібрид relative і fixed. Елемент поводиться як relative доки не досягне вказаної точки прокрутки, після чого "прилипає" як fixed — але тільки в межах батьківського контейнера.

.sticky-nav {
    position: sticky;
    top: 0; /* Прилипає до верху вікна при досягненні */
    /* Поки що — звичайний потік */
}
Preview
×
🔒localhost:3000

Зверніть: кожен заголовок розділу "прилипає" до верху при прокручуванні, але відразу йде, коли розділ закінчується. Це тому що sticky діє тільки в межах батьківського елемента (.sticky-section).

Популярні кейси sticky

  • Sticky header — навігаційна шапка, що залишається при прокрутці
  • Sticky sidebar — бічна панель, що прокручується разом зі сторінкою до певного моменту
  • Sticky table headers — заголовки таблиць, що залишаються видимими при великих таблицях
  • Sticky section titles — заголовки розділів у списках (приклад вище)
sticky не спрацює, якщо:
  1. Батьківський елемент має overflow: hidden або overflow: auto — елемент не зможе "вилізти" за межі контейнера.
  2. Не вказано жодну з властивостей top, right, bottom, left — браузер не знає, де "прилипати".
  3. Батьківський елемент надто маленький — sticky діє тільки доки є простір у батьку.

Порівняння всіх значень position

.element {
    position: static;
}
  • ✅ У нормальному потоці
  • ✅ Займає своє місце серед сусідів
  • top/right/bottom/left не діють
  • z-index не діє
  • Використання: за замовчуванням, скасування позиціонування
Властивістьstaticrelativeabsolutefixedsticky
У потоці✅/❌
top/left/... діють
z-index діє
Система відлікуВласна позиціяПозиціон. предокViewportБатьківський контейнер

top, right, bottom, left — зміщення

Ці властивості задають відстань від відповідного краю системи відліку:

.element {
    position: absolute;
    top: 20px; /* 20px від верхнього краю батька */
    right: 0; /* Впритул до правого краю */
    bottom: auto; /* auto = не задано */
    left: auto;
}

Важливо: top і bottom не сумуються — пріоритет має top. Аналогічно left має пріоритет перед right (для LTR-мов).

Трюк "розтягнути на весь батько"

.overlay {
    position: absolute;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
    /* Або скорочено: */
    inset: 0; /* Нова властивість (підтримка 2021+) */
}

inset: 0 — найелегантніший спосіб розтягнути absolute-елемент на весь батьківський блок:

Preview
×
🔒localhost:3000

Z-index: управління шарами

z-index (від "z-axis", вісь глибини) визначає порядок накладання позиціонованих елементів один на одного. Вища вартість — ближче до глядача.

.layer-1 {
    z-index: 1;
} /* Нижче */
.layer-2 {
    z-index: 10;
} /* Вище */
.layer-3 {
    z-index: 100;
} /* Найвище */

Правила z-index:

  1. Працює тільки на позиціонованих елементах (positionstatic) або на flex/grid-елементах.
  2. Приймає цілі числа (від'ємні теж дозволені).
  3. Елементи порівнюються в межах одного stacking context — ось тут починається найцікавіше.
Preview
×
🔒localhost:3000

Stacking Context — контекст накладання

Stacking context (контекст накладання) — це концепція, яка пояснює, чому z-index: 9999 іноді "не допомагає". Кожен stacking context — це незалежний стек шарів, і z-index порівнюється тільки між елементами одного контексту.

Метафора: папки на столі

Уявіть стіл із папками. Кожна папка — це stacking context. Усередині папки можуть бути аркуші з різними "пріоритетами". Але жоден аркуш із папки A не може бути між аркушами папки B — спочатку вирішується, яка папка "вище", а потім уже — що всередині папки.

Loading diagram...
graph TD
    Root["Root context (html)\nz-index: auto"] --> A["Контекст A\nz-index: 1"]
    Root --> B["Контекст B\nz-index: 2"]
    A --> A1["Елемент A1\nz-index: 9999"]
    A --> A2["Елемент A2\nz-index: 1"]
    B --> B1["Елемент B1\nz-index: 1"]

    classDef ctx fill:#6366f1,color:#fff
    classDef elem fill:#f59e0b,color:#fff
    class A,B ctx
    class A1,A2,B1 elem

У прикладі вище Елемент A1 (z-index: 9999) ніколи не перекриє Елемент B1 (z-index: 1), бо весь Контекст A (z-index: 1) знаходиться нижче Контексту B (z-index: 2). z-index: 9999 — це "найвищий у папці A", але папка A ціликом під папкою B.

Що створює новий stacking context?

position + z-index

position: relative/absolute/fixed/sticky з явним значенням z-index (не auto) — найпоширеніший випадок.

.ctx {
    position: relative;
    z-index: 1;
}

opacity < 1

Будь-який елемент з opacity менше одиниці автоматично стає новим stacking context.

.ctx {
    opacity: 0.99;
} /* Вже новий контекст! */

transform

Будь-яке transform відмінне від none. Саме тому transform "ламає" position: fixed.

.ctx {
    transform: translateZ(0);
}

filter

Будь-яке filter. Часто використовується для GPU-прискорення (filter: blur(0)).

.ctx {
    filter: drop-shadow(0 0 0);
}

isolation: isolate

Навмисне створення stacking context без візуальних ефектів — найчистіший спосіб.

.ctx {
    isolation: isolate;
}

will-change

will-change: transform, opacity та інші — підказка браузеру, яка також створює контекст.

.ctx {
    will-change: transform;
}

Практичне застосування: isolation: isolate

Коли у вас є компонент, який не повинен "заплутуватися" у z-index зовнішніх елементів — використовуйте isolation: isolate. Це явно створює stacking context без будь-яких візуальних побічних ефектів:

.modal-container {
    isolation: isolate; /* Ізолюємо z-index від зовнішнього світу */
}

.modal-overlay {
    z-index: 1; /* Фон */
}

.modal-dialog {
    z-index: 2; /* Діалог поверх фону */
}
/* Ніяких конфліктів із зовнішніми z-index! */

Практика: Modal overlay

Класична задача позиціонування — модальне вікно поверх всього сайту:

Preview
×
🔒localhost:3000

Розберемо архітектуру:

  • .modal-backdrop — займає весь viewport (position: fixed; inset: 0; z-index: 1000) і стає напівпрозорим затемненням. Клік на нього закриває модальне.
  • .modal-dialogposition: relative тут потрібен для кнопки закриття (.modal-close). Сам діалог центрується через flex на батьківському backdrop.
  • .modal-closeposition: absolute; top: 1rem; right: 1rem — класичне розміщення кнопки закриття у правому верхньому куті.
  • backdrop-filter: blur(2px) — сучасний ефект розмиття фону за допомогою одного рядка CSS.

Практика: Dropdown Menu

Ще один класичний кейс — випадаюче меню (dropdown):

Preview
×
🔒localhost:3000

Ключові аспекти реалізації:

  • .dropdown { position: relative } — задає систему відліку.
  • .dropdown-menu { position: absolute; top: calc(100% + 6px) } — меню відразу під кнопкою, з відступом 6px.
  • opacity: 0 + pointer-events: none → відображається при :hover — CSS-only підхід без JavaScript.
  • transform: translateY(-8px)translateY(0) — плавна анімація появи знизу вгору.
  • :focus-within — меню залишається відкритим, поки фокус знаходиться всередині блоку (клавіатурна навігація).

Sticky Header: практичний патерн

Фіксований хедер — один із найпоширеніших UI-патернів:

/* === Sticky Header === */
.site-header {
    position: sticky;
    top: 0;
    background: white;
    box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1); /* З'являється при прокрутці */
    z-index: 100;

    /* Внутрішнє компонування — Flexbox */
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 0 2rem;
    height: 64px;
}

/* Відступ для основного контенту,
   щоб хедер не перекривав початок сторінки: */
.site-main {
    padding-top: 0; /* Не потрібен для sticky (він у потоці) */
    /* Але для fixed — обов'язковий! */
}
Для sticky header перевага — він у нормальному потоці, і ви не мусите додавати padding-top до основного контенту. Для fixed header — обов'язково додайте padding-top: [висота хедера] до body або main, інакше хедер перекриє початок контенту.

Налагодження позиціонування в DevTools

Chrome DevTools — незамінний інструмент для розуміння позиціонування:

Перевірте, чи позиціонований елемент

У вкладці Styles знайдіть рядок position. Якщо statictop/left/z-index ігноруються.

Знайдіть containing block

Виберіть position: absolute-елемент і у вкладці Computed перейдіть до Containing Block. DevTools покаже, відносно якого предка здійснюється позиціонування.

Перевірте stacking context

Якщо z-index не працює — виберіть елемент і перевірте батьків на transform, opacity < 1, filter. Будь-який із них — це новий stacking context.

Використовуйте підсвічування

Наведіть курсор на елемент у панелі Elements — браузер підсвітить область елемента та його позиціоновані нащадки.


Резюме: ієрархія позиціонування

Loading diagram...
flowchart TD
    Start["Потрібно позиціонувати елемент"] --> Q1{"Елемент повинен\nйти с потоку?"}
    Q1 -->|Ні| Rel["position: relative\n• Дрібне зміщення\n• Системa відліку для absolute"]
    Q1 -->|Так| Q2{"Відносно чого?"}
    Q2 -->|Предка| Abs["position: absolute\n• Відносно найближчого\n  позиціонованого предка\n• Tooltips, badges, menus"]
    Q2 -->|Viewport| Q3{"Прокручується\nзі сторінкою?"}
    Q3 -->|Ні| Fix["position: fixed\n• Завжди в viewport\n• Cookie banner, chat"]
    Q3 -->|Так потім ні| Stic["position: sticky\n• Scroll hybrid\n• Sticky header, sections"]

Завдання для самоперевірки


Попередня стаття: CSS Grid. Частина 2

Наступна стаття: Адаптивний дизайн. Media Queries

Copyright © 2026