CSS запити контейнерів
CSS container queries (контейнерні запити) дозволяють стилізувати елементи на основі розміру батьківського контейнера, а не вікна браузера.
Теорія
TL;DR
- Media queries стежать за розміром вікна браузера; container queries — за розміром батьківського елемента.
- Аналогія: media queries як центральна система опалення (весь будинок реагує однаково); container queries як особистий термостат (кожен пристрій адаптується до свого місця).
- Без
container-type: inline-sizeна батьківському елементі@containerнічого не зробить. - Container queries для компонентів, які повторно використовуються; media queries для глобальних макетів сторінки.
- Підтримка браузерами: Chrome 105+, Firefox 110+, Safari 16+.
Швидкий приклад
Два контейнери, різна ширина. Той самий .card всередині кожного. Тільки широкий активує зміну макету:
.parent {
container-type: inline-size; /* оголошуємо контейнер для запитів */
}
@container (min-width: 400px) {
.card {
display: flex;
flex-direction: row; /* горизонтально тільки у широких контейнерах */
}
}<div class="parent" style="width: 300px">
<div class="card">Вузький: залишається вертикальним</div>
</div>
<div class="parent" style="width: 500px">
<div class="card">Широкий: стає горизонтальним</div>
</div>Картка у 300px контейнері залишається вертикальною. Картка у 500px контейнері перемикається на горизонтальний макет. Розмір вікна браузера тут не має значення.
Ключова різниця
@media перевіряє розміри window через matchMedia(). Уся сторінка ділить один viewport, тому кожен компонент на @media прив'язаний до контексту сторінки. @container працює на рівні окремого елемента: браузер створює список запитів для кожного оголошеного контейнера і обчислює їх незалежно. Той самий .card може бути вузьким у бічній панелі і широким у hero-секції без жодного додаткового CSS.
Коли що використовувати
- Картка, яка з'являється і в бічній панелі, і в основній сітці: container queries (кожен адаптується локально).
- Віджети дашборду різної ширини: container queries (кожен обчислюється незалежно).
- Навігація, яка згортається на мобільному: media queries (це глобальне рішення для макету).
- Стилі для друку або орієнтації екрана: media queries (рівень пристрою, не компонента).
Найпростіше правило: якщо компонент використовується в різних макетах, container queries. Якщо змінюєш структуру самої сторінки, media queries.
Таблиця порівняння
| Ознака | Media Queries | Container Queries |
|---|---|---|
| Тригер | Розмір viewport (браузера) | Розмір батьківського контейнера |
| Синтаксис | @media (min-width: 768px) | @container (min-width: 400px) |
| Область дії | Глобальна (вся сторінка) | Локальна (окремий контейнер) |
| Підтримка браузерами | IE9+ | Chrome 105+, Firefox 110+, Safari 16+ |
| Найкраще для | Макети сторінок, теми, друк | Компоненти в сітках і flex-макетах |
Як це працює всередині
Браузер створює об'єкт ContainerQuery для кожного елемента з оголошеним container-type. Під капотом механізм схожий на ResizeObserver: коли inline або block розмір контейнера змінюється, рушій перераховує список правил @container і оновлює стилі. Якщо нічого не змінилось, reflow не відбувається. Контейнер також встановлює контекст ізоляції (containment context), який відокремлює піддерево від батьківського макету до того, як запит спрацьовує. Саме тому контейнер не може запитувати власний розмір: специфікація запобігає циклічній залежності на межі containment.
Одиниці вимірювання контейнерних запитів
Container queries мають власні одиниці довжини:
cqw— 1% ширини контейнераcqh— 1% висоти контейнераcqi— 1% inline розміру контейнераcqb— 1% block розміру контейнераcqmin/cqmax— менше або більше зcqiіcqb
.card-container {
container-type: inline-size;
}
.card {
font-size: clamp(1rem, 4cqw, 2rem); /* масштабується з контейнером, не з viewport */
padding: clamp(1rem, 5cqw, 3rem);
}Для компонентів всередині інших елементів це передбачуваніше ніж одиниці vw.
Іменовані контейнери
Якщо на сторінці кілька контейнерів, дай їм імена щоб контролювати, який @container запит до якого відноситься:
.sidebar {
container-type: inline-size;
container-name: sidebar;
}
.main-content {
container-type: inline-size;
container-name: main;
}
@container sidebar (min-width: 300px) {
.card { /* спрацьовує тільки всередині .sidebar */ }
}
@container main (min-width: 600px) {
.card { /* спрацьовує тільки всередині .main-content */ }
}Без імені @container запитує найближчого предка з container-type.
Типові помилки
Забули container-type на батьківському елементі. Найпоширеніша проблема. Без оголошеного контейнера правила @container просто ігноруються. Ні помилки, ні попередження, просто нічого не відбувається.
/* неправильно: контейнер не оголошено, @container ігнорується */
@container (min-width: 400px) {
.card { display: flex; }
}
/* правильно */
.parent {
container-type: inline-size;
}
@container (min-width: 400px) {
.card { display: flex; }
}Використовуєш block-size, коли потрібен inline-size. container-type: block-size працює, але потрібен рідко і має нерівномірну підтримку (Firefox додав у 121). У більшості випадків inline-size закриває потребу. Якщо треба запитувати обидва виміри, використовуй container-type: size.
Вкладені контейнери у flex/grid без ізоляції. Якщо дочірній контейнер змінює розмір, він може ініціювати повторне обчислення батьківського контейнера, що знову змінює розмір дочірнього. Chrome 117+ додав self-contained для розриву цього циклу:
.inner-container {
container-type: inline-size self-contained;
}Запити до замалих контейнерів. @container (min-width: 300px) на 20px іконці ніколи не спрацює. Перевір фактичну мінімальну ширину контейнера перед тим як писати проти нього запити.
Використовують style queries без style у container-type. Контейнерні запити стилів (Chrome 111+) дозволяють запитувати значення кастомних властивостей, але style має бути у container-type:
.card-container {
container-type: style inline-size; /* 'style' обов'язковий */
}
@container style(--variant: dark) {
.card { background: #111; color: #fff; }
}Де зустрічається в реальних проектах
- Shadcn/UI — картки використовують
@containerщоб адаптуватись у будь-якому макеті без перевизначень для кожного контексту. - Tailwind CSS v3.2+ — є утиліти для
container-typeі офіційний плагін для@container. - Chakra UI — тайли дашборду у flex-сітках використовують container queries щоб складатись або розгортатись залежно від ширини тайла.
- Headless UI — вміст модальних вікон адаптується до ширини діалогу, а не viewport.
Я почав використовувати container queries після того як витратив забагато часу на боротьбу з @media брейкпоінтами в бібліотеці компонентів: картки з'являлись одночасно в бічних панелях, сітках і повноширинних банерах. Один набір правил @container замінив три набори макет-специфічних перевизначень.
Питання для співбесіди
Q: Яка різниця між container-type: inline-size і size?
A: inline-size відстежує тільки ширину. size відстежує і ширину, і висоту. Використовуй size тільки коли запит має реагувати на вертикальні зміни теж, бо він створює сильніші обмеження containment.
Q: Чому контейнер не може запитувати власний розмір?
A: Виникає циклічна залежність: розмір елемента залежить від дочірніх, які залежать від результату запиту, який залежить від розміру елемента. Специфікація розриває цей цикл, вимагаючи встановлення containment до обчислення запиту.
Q: Як container queries взаємодіють з CSS containment?
A: Оголошення container-type неявно додає contain: layout style (або contain: layout style size для типу size). Це ізолює піддерево і запобігає зсуву батьківського макету при зміні дочірніх елементів.
Q: Чи є поліфіл?
A: Повного поліфілу немає. Фіча спирається на ResizeObserver на рівні браузерного рушія. Обхідний шлях: JS ResizeObserver що додає і видаляє класи, і CSS-правила на основі класів як запасний варіант.
Q: Продуктивність: 100 container queries проти 100 media queries?
A: Обидва механізми використовують observer-based обчислення. На практиці 100+ контейнерів означає 100+ спостерігачів. Поєднуй з content-visibility: auto для позаекранних секцій щоб обмежити кількість активних спостерігачів.
Q (senior): У сітці з 50 однакових карток, як зменшити вартість обчислення container queries підтримуючи при цьому style queries?
A: Оголоси container-type: style size self-contained на спільному предку замість кожної картки окремо. Один спостерігач покриває предка. Картки читають кастомну властивість --theme предка через style query. За трасами Chromium DevTools це скорочує з 50 незалежних спостерігачів до одного на групу.
Приклади
Дві картки, один компонент, різні контейнери
Той самий компонент, той самий CSS, різний результат залежно від ширини батька.
<style>
.parent {
container-type: inline-size;
border: 1px solid #ccc;
margin: 20px;
padding: 10px;
}
.narrow { width: 300px; }
.wide { width: 560px; }
.card {
background: #e8f4ff;
padding: 16px;
display: flex;
flex-direction: column; /* за замовчуванням: вертикально */
gap: 12px;
}
@container (min-width: 450px) {
.card {
flex-direction: row; /* горизонтально у широких контейнерах */
align-items: center;
}
}
</style>
<div class="parent narrow">
<div class="card">
<img src="avatar.jpg" width="48" height="48" alt="Користувач">
<div>
<strong>Ганна Ковальчук</strong>
<p>Product designer</p>
</div>
</div>
</div>
<div class="parent wide">
<div class="card">
<img src="avatar.jpg" width="48" height="48" alt="Користувач">
<div>
<strong>Ганна Ковальчук</strong>
<p>Product designer</p>
</div>
</div>
</div>Вузький батько (300px): зображення над текстом. Широкий батько (560px): зображення ліворуч, текст праворуч. Розмір вікна браузера не задіяний.
Віджет дашборду з іменованим контейнером і одиницями cqi
Віджет, який з'являється і в слоті бічної панелі (240px), і в повноширинній зоні (900px). Без перевизначень для кожного контексту.
// DashboardCard.jsx
function DashboardCard({ title, value, trend }) {
return (
<div className="widget">
<div className="chart">
<h3 className="chart__title">{title}</h3>
<span className="chart__metric">{value}</span>
<span className="chart__trend">{trend}</span>
</div>
</div>
);
}.widget {
container-type: inline-size;
container-name: dashboard-card;
}
.chart {
padding: 16px;
display: grid;
grid-template-columns: 1fr; /* одна колонка за замовчуванням */
gap: 8px;
}
.chart__metric {
font-size: clamp(1.25rem, 6cqi, 2.5rem); /* fluid, прив'язаний до ширини контейнера */
}
@container dashboard-card (min-width: 450px) {
.chart {
grid-template-columns: 1fr 2fr; /* графік і деталі поруч */
}
}
@container dashboard-card (min-width: 600px) {
.chart__metric {
font-size: 2.5rem;
}
}Одиниця cqi масштабує текст метрики відносно inline розміру контейнера, а не viewport. Зміни ширину слоту і текст масштабується разом з ним.
Style queries для тематичних компонентів
Style queries (Chrome 111+) дозволяють дочірнім елементам читати кастомні властивості з контейнера. Без передачі пропсів через кожен шар розмітки.
.card-container {
container-type: style inline-size; /* 'style' вмикає style queries */
container-name: card-wrapper;
}
.card {
background: #fff;
color: #111;
padding: 20px;
border-radius: 8px;
}
@container card-wrapper style(--variant: dark) {
.card {
background: #1a1a2e;
color: #e0e0e0;
}
}
@container card-wrapper style(--variant: warning) {
.card {
background: #fff3cd;
color: #856404;
}
}<div class="card-container" style="--variant: dark">
<div class="card">Картка з темною темою</div>
</div>
<div class="card-container" style="--variant: warning">
<div class="card">Картка попередження</div>
</div>
<div class="card-container">
<div class="card">Картка за замовчуванням</div>
</div>Кастомна властивість --variant на контейнері слугує сигналом. Картка читає його без жодного JS. Якщо пропустити style у container-type, запит ніколи не спрацює і всі картки показуватимуть стилі за замовчуванням.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.