Контейнерні запити в CSS: як не думати про них як про media queries
У матеріалі розбирається підхід Віктора Айоміпо до контейнерних запитів у CSS — чому їх не варто сприймати як «просунуті media queries» і як вони змінюють мислення про адаптивні інтерфейси.
Media queries: погляд назовні
Media queries історично навчили всіх думати: «екран ширше — вмикаємо десктоп-версію, вужчий — мобільну». Коли ви пишете @media, по суті ви питаєте браузер: «Яка зараз ширина вікна?» — і приймаєте рішення про весь макет цілком.
Типовий приклад:
@media (min-width: 1024px) {
.card {
display: flex;
}
Запит працює бездоганно з точки зору своєї задачі: якщо viewport має хоча б 1024px, стилі спрацьовують. Проблема виникає, коли .card знаходиться, скажімо, в сітці, де їй виділено лише 300px ширини. Media query цього не бачить: воно знає тільки про вікно браузера, і компонент «вважає себе» широким, навіть якщо фактично йому тісно.
Саме тому автор наводить думку, що media queries «тупуваті» не за ідеєю, а за обсягом інформації: вони знають лише про медіа-контекст (вікно, тип носія, системні налаштування на кшталт prefers-color-scheme), але повністю сліпі до того, що відбувається всередині компонента.
У цьому підході media queries логічно використовувати як інструмент для «макро»-макета:
- загальна сітка сторінки;
- хедер/футер, що тягнуться на всю ширину;
- перемикання колонок (1–2–3 колонки в залежності від розміру екрана);
- зміна стилів під системні або девайсні налаштування.
Контейнерні запити: погляд усередину
Контейнерні запити перевертають точку відліку. Вони відповідають не на питання «який розмір екрана?», а на питання: «скільки простору є в мене, у цьому конкретному контейнері, просто зараз?». Тобто логіка адаптації переноситься з viewport на найближчий батьківський елемент.
Той самий приклад з карткою можна переписати через контейнерний запит:
.card-wrapper {
container-name: card;
container-type: inline-size;
}
@container card (min-width: 450px) {
.card {
display: flex;
flex-direction: row;
}
}
Тепер .card реагує не на ширину вікна, а на ширину свого контейнера .card-wrapper. Якщо контейнер має щонайменше 450px по inline-вісі, картка розкладається в ряд; якщо менше — працює її «компактний» стан. Один і той самий компонент коректно поводиться в широкій сітці, вузькому сайдбарі або будь-якому іншому місці, де б він не з’явився.
Звідси пропоноване автором розділення:
- macro-layout — сторінковий рівень, де доречні
@media; - micro-layout — рівень компонентів, де краще працюють
@container.
Компонент не повинен ставати «планшетним», лише тому що ширина вікна перевищила абстрактні 768px. Він має змінюватися тільки тоді, коли йому вистачає місця. Інакше один і той самий блок поводитиметься дивно в різних зонах сторінки.
Приклад 1: плинна типографіка всередині компонента
Багато хто привчився регулювати розміри шрифтів через viewport-одиниці (vw, vh) й clamp. Це працює, поки компонент існує в єдиному контексті (наприклад, тільки в основній колонці).
Класичний приклад плинної типографіки виглядає так:
.card-title {
font-size: clamp(100%, 1rem + 2vw, 24px);
}
Тут розмір заголовка залежить від ширини вікна. Але щойно компонент переміщують, наприклад, у вузький сайдбар в інтерфейсі десктопного застосунку, viewport лишається тим самим, а реально доступний простір для заголовка — ні. У результаті він може стати або надто великим, або надто дрібним.
Контейнерні запити розширюють цей підхід за рахунок власних одиниць вимірювання (cqi, cqw, cqb тощо), прив’язаних до розмірів контейнера. Їх можна поєднувати з clamp(), щоб масштабувати шрифти відносно компонента:
.card-title {
font-size: clamp(1rem, .5rem + 3cqi, 2rem);
}
У цьому варіанті розмір шрифту залежить від inline-розміру контейнера, а не вікна. Компонент стає самодостатнім: куди б ви його не вставили, типографіка поводиться очікувано, без додаткових «перевизначень для сайдбарів» чи окремих класів.
Приклад 2: умовне «виявлення wrap’у» у flex-верстці
Ще один показовий кейс, який згадує автор, — робота з flex-версткою, що обгортається (flex-wrap: wrap). У класичному CSS немає ні :wrapped, ні будь-якого селектора або media query, що «відчує», коли елементи перейшли на новий рядок. Без JS (наприклад, без ResizeObserver) це важко проконтролювати.
За допомогою контейнерних запитів можна обійтися без JavaScript у частині сценаріїв. Ідея, яку автор взяв із прийому Кевіна Пауелла: зробити кожен flex-елемент контейнером і використовувати його розширення після wrap’у як тригер для стилів.
Схема така:
- Поки достатньо місця, елементи-
.flex-itemстоять в одному рядку і ділять ширину контейнера. - Як тільки місця бракує, другий елемент переноситься на новий рядок.
- Через
flex: 1 1 Xpxобидва елементи розтягуються, заповнюючи ширину по максимуму у своєму рядку. - Оскільки
.flex-itemзареєстрований як контейнер, зміна його ширини може стати умовою в@container.
Спрощений варіант цієї ідеї виглядає так:
Коли елемент ще «вузький» і стоїть у ряд з іншим, @container не спрацьовує — картка у колонковому режимі. Коли простору стає мало і елементи переносяться, кожен з них розтягується, контейнери досягають 600px і вище — і картки перемикаються в «широкий» горизонтальний вигляд.
Це не «справжнє» виявлення wrap’у, скоріше інженерний обхідний маневр, але media queries подібного зробити не можуть в принципі, бо не бачать внутрішніх змін у макеті.
Побічні ефекти й обмеження контейнерних запитів
1. Потрібен додатковий рівень обгортки
Контейнер не може запитувати сам себе — це породжує потенційну циклічну залежність. Тому необхідний чіткий зв’язок «контейнер → нащадки».
Якщо є розмітка:
і спробувати одночасно оголосити .card контейнером і запитувати його ж розмір у @container, як нижче, це не спрацює:
Щоб прив’язати стилі до розміру, потрібен окремий контейнер, у якому .card є нащадком:
і вже цей контейнер стає об’єктом запиту:
У media queries такого обмеження немає: @media завжди дивиться на viewport, тому ви вільно стилізуєте будь-який елемент усередині блоку.
2. container-type: size може зламати висоту
Коли ви оголошуєте контейнер за повним розміром (container-type: size), браузер має вирахувати його ширину і висоту без огляду на вміст. Якщо явної висоти немає, це призводить до того, що блок може «злипнутися» до 0px по вертикалі:
Оскільки висота не задана (ані height, ані min-height, ані aspect-ratio), браузер вважає її нульовою. У результаті макет ламається, навіть якщо всередині є контент.
Практичний висновок автора: у більшості випадків варто працювати з inline-size, а до size звертатися обережно й тільки тоді, коли справді потрібна реакція і на ширину, і на висоту контейнера, і ви явно керуєте цими розмірами.
3. Неможливо використовувати custom properties у самій умові
Ще одне важливе обмеження: контейнерні запити зараз не дозволяють використовувати CSS-змінні безпосередньо в умові порівняння.
Наприклад, такий код не працює:
Причина — у природі кастомних властивостей: вони залежать від каскаду й можуть змінюватися всередині того ж блоку, де ви їх використовуєте в умові. Автор показує, до якого абсурду це може призвести:
Контейнерний запит залежав би від змінної, значення якої він же змінює нижче по дереву. Це створює потенційно нерозв’язний цикл, тому стандарт забороняє таке використання. Для контейнерів доводиться працювати з «жорстко» заданими значеннями або іншими прийомами.
Коли обирати media queries, а коли — container queries
Автор не закликає відмовлятися від @media. Швидше навпаки: йдеться про чіткий поділ зон відповідальності.
Коли логічно використовувати media queries:
- компонент існує тільки на рівні сторінки (наприклад, головна навігація зверху, що завжди тягнеться на всю ширину);
- поведінка прямо пов’язана з розміром або орієнтацією viewport;
- потрібна реакція на системні налаштування або тип пристрою.
Коли варто подумати про container queries:
- компонент використовується в кількох різних контекстах (сітка, сайдбар, картки в різних блоках);
- важливо, щоб поведінка залежала від реальної площі, що виділена під цей компонент, а не від абстрактної ширини екрана;
- потрібна «самодостатня» логіка компонента (типографіка, розкладка елементів усередині, стан, залежний від wrap’у тощо).
Іншими словами, viewport — це проксі, який часто зручний для макро-рівня, але що глибше ви занурюєтесь у компоненти, то ближче логіка адаптації до безпосереднього контейнера.
Підсумок
За формою синтаксис @container сильно нагадує @media, через що багато розробників інтуїтивно намагаються використати новий інструмент «по-старому». Підхід, який описує Віктор Айоміпо, полягає не стільки в новій техніці верстки, скільки в зміні моделі мислення: розглядати адаптивність як властивість компонента і його контейнера, а не лише ширини вікна.
Media queries лишаються ключовим інструментом для «макро»-рівня сторінки, системних налаштувань і загальної структури. Контейнерні запити доповнюють їх там, де компонент має жити в різних контекстах і сам визначати, коли йому «тісно» або «просторо». Саме в такій ролі, за логікою автора, вони розкривають свій повний потенціал.
Джерело: Victor Ayomipo — Smashing Magazine