Container queries: адаптив по контейнеру, а не по экрану
Одна карточка, которая правильно выглядит и в узком сайдбаре, и в широкой сетке — при одном и том же размере окна.
Что ты научишься делать
- Поймёшь, почему медиазапросов недостаточно для компонентов
- Настроишь контейнер и напишешь запрос к его ширине
- Сделаешь карточку, которая перестраивается сама в любом месте страницы
#Компонент не знает, где он окажется
Ты сверстал карточку товара: картинка слева, текст справа. На широком экране красиво.
Потом эту же карточку положили в сайдбар шириной 280 пикселей. Картинка и текст сжались до нечитаемого. Медиазапрос не помогает: экран-то широкий, узкий здесь только контейнер.
Обычный ответ — завести модификатор: .card--narrow. Он работает ровно до того момента, пока кто-то не положит карточку в третье место, где ширина ещё какая-то.
Так не надо
/* Компонент зависит от места, куда его положат */
.card { grid-template-columns: 140px 1fr; }
.sidebar .card { grid-template-columns: 1fr; }
.footer .card { grid-template-columns: 1fr; }
.modal .card { grid-template-columns: 100px 1fr; }Каждое новое место — новая строка. Компонент перестал быть самостоятельным: чтобы им пользоваться, надо знать все места, где он встречается.
Так надо
/* Компонент решает сам, по своей ширине */
@container card (min-width: 420px) {
.card { grid-template-columns: 140px 1fr; }
}Одно правило на все случаи. Положи карточку куда угодно — она разберётся.
#Два шага
Сначала нужно объявить контейнер — сказать браузеру, что за размером этого элемента будут следить. Потом написать запрос.
/* Шаг 1: обёртка становится контейнером */.card-slot { container-type: inline-size; container-name: card;}/* Шаг 2: правило срабатывает по ширине контейнера */@container card (min-width: 420px) { .card { grid-template-columns: 140px 1fr; align-items: center; }}container-type: inline-size — следим за шириной. Это самый частый вариант: высота обычно определяется содержимым, и следить за ней опасно (получится замкнутый круг).
container-name необязателен, но с именем понятнее: запрос без имени привяжется к ближайшему контейнеру, а это не всегда тот, что нужен.
#Единицы контейнера
Вместе с запросами появились единицы, считающиеся от контейнера, а не от окна. Они полезнее, чем кажется: с ними размер шрифта внутри компонента можно привязать к его собственной ширине.
| Единица | От чего считается |
|---|---|
cqw | 1 % ширины контейнера |
cqh | 1 % высоты контейнера |
cqi | 1 % размера вдоль строки — обычно то же, что cqw |
cqmin, cqmax | Меньшая и большая из двух сторон |
.card__title { /* Не меньше 16px, не больше 24px, между ними — по ширине контейнера */ font-size: clamp(1rem, 4cqw, 1.5rem);}#Как это выглядит в реальном проекте
В работающей системе компонентов запросы к контейнеру и медиазапросы делят обязанности. Медиазапрос отвечает за страницу: сколько колонок в раскладке, показывать ли сайдбар. Запрос к контейнеру отвечает за компонент: как он собран внутри.
/* Страница решает раскладку — это работа медиазапроса */.page { display: grid; gap: 24px;}@media (min-width: 1024px) { .page { grid-template-columns: 1fr 320px; }}/* Слоты объявлены контейнерами один раз, на всю систему */.page > *,.sidebar > * { container-type: inline-size; container-name: slot;}/* Компонент подстраивается сам — и в контенте, и в сайдбаре */@container slot (min-width: 380px) { .post-card { grid-template-columns: 160px 1fr; } .post-card__excerpt { display: block; }}@container slot (max-width: 379px) { /* В узком месте лишнее просто убираем */ .post-card__excerpt { display: none; }}#Что меняется, когда элемент становится контейнером
container-type — не безобидный флаг. Он включает у элемента ограничение размера по нужной оси: содержимое перестаёт влиять на ширину контейнера.
Обычно это незаметно, потому что блочный элемент и так занимает всю доступную ширину. Но если контейнером сделать что-то, что раньше сжималось по содержимому — например, элемент внутри флекс-строки или таблицу, — он внезапно растянется или схлопнется.
| Что было | Что станет после container-type: inline-size |
|---|---|
| Блок во всю ширину | Ничего не изменится — самый частый и безопасный случай |
| Элемент, сжимавшийся по содержимому | Начнёт занимать всю доступную ширину |
| Плавающий или абсолютный блок | Может схлопнуться: ширину больше неоткуда взять |
| Таблица | Раскладка таблицы сломается — таблицы контейнерами не делают |
#Когда НЕ надо
- Для раскладки всей страницы. Сколько колонок на странице и нужен ли сайдбар — вопрос про экран, а не про контейнер. Это работа медиазапросов, и она никуда не делась.
- Когда компонент живёт в одном месте. Шапка сайта всегда во всю ширину. Заводить для неё контейнер — лишний слой без пользы.
- Следить за высотой без нужды.
container-type: sizeтребует, чтобы высота была задана явно, иначе браузер не сможет её посчитать. Ошибиться легко, а отладить трудно; в подавляющем большинстве случаев нуженinline-size. - Ради «модно». Контейнер создаёт новый контекст: элементы внутри перестают влиять на размер контейнера по нужной оси. Это может неожиданно сломать соседние элементы.
#Поддержка и фоллбэк
| Возможность | Состояние |
|---|---|
@container по размеру | Во всех современных браузерах с 2023 года |
Единицы cqw, cqi | Вместе с самими запросами |
| Именованные контейнеры | Вместе с самими запросами |
Запросы по стилю (style()) | Появились позже и поддерживаются не везде — на них пока не закладывайся |
/* Базовый вид — узкий. Он же фоллбэк для старых браузеров */.card { display: grid; grid-template-columns: 1fr;}/* Улучшение приходит только там, где запросы понимают */@container card (min-width: 420px) { .card { grid-template-columns: 140px 1fr; }}Где спотыкаются почти все
Контейнером назначен сам компонент
Почему Элемент не может отвечать на запрос о собственном контейнере. Правило внутри @container просто не сработает, и причина неочевидна.
Как надо Контейнер — обёртка или слот раскладки. Перестраивается то, что лежит внутри.
Забыт container-type
Почему Одного container-name мало: без типа элемент контейнером не становится, а запрос молча привязывается к другому предку или ни к чему.
Как надо Всегда пиши обе строки, а лучше сокращение: container: card / inline-size;
Взят container-type: size без заданной высоты
Почему Браузер должен знать высоту заранее — иначе получается круг: высота зависит от содержимого, а содержимое от высоты. Контейнер схлопывается.
Как надо Бери inline-size, если нет твёрдой причины следить за высотой.
Запросы к контейнеру заменили медиазапросы полностью
Почему Раскладка страницы и настройки вроде тёмной темы или prefers-reduced-motion к контейнерам отношения не имеют.
Как надо Страница — медиазапросы, компонент — контейнерные. Они дополняют друг друга.
- Контейнерный запрос container query
- Правило, срабатывающее по размеру элемента-предка, а не окна.
- Тип контейнера container-type
- За каким размером следить:
inline-size— за шириной,size— за обеими сторонами. - Единицы контейнера cqw, cqh, cqi
- Проценты от размера контейнера. Позволяют привязать шрифт и отступы к ширине компонента.
Проверь себя
Экран может быть широким, а места компоненту досталось мало. Про это медиазапрос ничего не знает.
Без <code>container-type</code> элемент контейнером не становится, и запросу не к чему привязаться.
Иначе получился бы замкнутый круг: стиль менял бы ширину, ширина меняла бы стиль. Поэтому компонент оборачивают.
Незнакомое @-правило игнорируется целиком. Если базовый вид рабочий, ничего чинить не нужно.
Верных вариантов несколько.
Всё, что про экран и настройки человека, — медиазапросы. Контейнерные отвечают только за внутреннее устройство компонента.
Задание
Карточка для двух мест сразу
Одна и та же карточка стоит в узком слоте (200px) и в широком, растянутом на всю доступную ширину. Сейчас она везде одинаковая и в широком месте выглядит бедно.
Сделай так, чтобы обёртка стала контейнером с именем card, а начиная с ширины 280px карточка перестраивалась в две колонки 110px 1fr: картинка слева, текст справа. Разметку не трогай.
Подсказка 1
Контейнер объявляется на обёртке: .slot { container-type: inline-size; container-name: card; }
Подсказка 2
Запрос пишется так: @container card (min-width: 280px) { .card { grid-template-columns: 110px 1fr; } }
Показать решение
<div class="row"> <div class="slot slot--narrow"> <article class="card"> <div class="media"></div> <div class="body"><h3>Раф</h3><p>Мягкий вкус</p></div> </article> </div> <div class="slot slot--wide"> <article class="card"> <div class="media"></div> <div class="body"><h3>Раф</h3><p>Мягкий вкус</p></div> </article> </div></div>.slot { container-type: inline-size; container-name: card;}@container card (min-width: 280px) { .card { grid-template-columns: 110px 1fr; align-items: center; } .media { height: 100%; min-height: 90px; }}.row { display: grid; gap: 14px; }.slot--narrow { inline-size: 200px; }.slot--wide { inline-size: 100%; }.card { display: grid; gap: 10px; padding: 12px; background: #f5f5fa; border-radius: 12px;}.media { height: 70px; background: linear-gradient(135deg, #7c5cff, #b06cff); border-radius: 8px;}.body h3 { margin: 0 0 4px; font-size: 16px; }.body p { margin: 0; font-size: 13px; color: #5b6070; }