Статья · Личный блог
Контейнерные запросы: компонент реагирует на свой слот, а не на экран
Переход от медиазапросов к @container, контейнмент по inline-size, единица cqi и грабли, на которые натыкается карточка в сайдбаре.
Годами карточка в широкой основной колонке и та же карточка, втиснутая в узкий сайдбар, требовали двух разных наборов стилей, склеенных медиазапросом, который не знал ни про один из этих слотов. Контейнерные запросы наконец дали компоненту задать единственный важный вопрос: сколько места у меня тут на самом деле есть?
От вьюпорта к слоту
Медиазапрос отвечает на глобальный вопрос — какова ширина экрана — и все компоненты на странице слышат один и тот же ответ. Но карточке нет дела до экрана. Ей важна колонка, в которую её положили. Десктоп на 1440px может одновременно держать сайдбар на 240px и основную область на 900px, и один и тот же компонент рендерится в обоих.
С медиазапросами слот кодируется косвенно: если экран широкий, значит есть сайдбар, значит карточка в нём узкая. Эта связка ломается в тот момент, когда кто-то переиспользует карточку там, где автор раскладки и не думал.
Медиазапрос спрашивает, насколько велика страница. Контейнерный запрос спрашивает, насколько велик слот компонента. Только второй вопрос — дело самого компонента.
container-type и зачем нужен контейнмент
Чтобы запрашивать элемент, его сначала объявляют контейнером. Обычный случай — inline-size: браузер отслеживает inline-размер контейнера (ширину в горизонтальных режимах письма) и отдаёт его в правила @container потомков.
.card-slot {
container-type: inline-size;
container-name: card;
}
@container card (min-width: 30rem) {
.card { grid-template-columns: 12rem 1fr; } /* широкий слот: медиа рядом с текстом */
}
Почему вообще должен участвовать контейнмент? Потому что раскладка, где контейнер подгоняет себя под детей, а дети подгоняют себя под контейнер, циклична. container-type: inline-size включает size containment по inline-оси: inline-размер элемента вычисляется независимо от его содержимого, и цикл разрывается. Это же и ограничение, которое стоит помнить, — контейнерный элемент больше не растягивается под своих детей по этой оси.
Единица cqi и компания
Контейнерные запросы приносят единицы длины, относительные к контейнеру, — и это часть, которую недоиспользуют. 1cqi — это 1% inline-размера контейнера запроса; 1cqw — 1% его ширины. Есть cqb и cqh для блочной оси, плюс cqmin и cqmax.
Это позволяет масштабировать типографику и отступы к слоту, а не к экрану — флюидная типографика наконец следит за компонентом, а не за окном:
.card__title { font-size: clamp(1rem, 0.8rem + 2.5cqi, 1.5rem); line-height: 1.2; }
В сайдбаре на 240px этот clamp разрешается в мелкий размер; в колонке на 900px та же декларация увеличивает заголовок — без брейкпоинта и без JavaScript, который что-то измеряет.
Нельзя запросить элемент, который ты сделал контейнером
Это те грабли, которые стоят каждому половины рабочего дня. Правило @container сопоставляется с ближайшим предком-контейнером, а не с элементом, несущим container-type. Поэтому нельзя повесить container-type на .card, а потом писать правила @container, стилизующие сам .card по его собственному размеру.
Решение структурное: контейнер — это обёртка, а то, что ты перестилизуешь, — потомок.
- Вешайте
container-typeна слот или обёртку, но не на корень компонента, который хотите адаптировать. - Давайте вложенным контейнерам
container-name, чтобы внутренний@containerслучайно не разрешился к чужому предку. - Помните, что каскад не меняется —
@containerлишь решает, какие правила применятся; победителя по-прежнему определяет специфичность.
В файловом менеджере, над которым я работал, карточка превью жила в трёх разных слотах — сайдбар, модалка, ячейка сетки — с одинаковой разметкой. Контейнером владела обёртка; карточка просто реагировала. Один компонент, три контекста, ноль условного рендеринга.
Чего это стоит и где кусается
Size containment не бесплатен концептуально. Как только элемент стал size-контейнером по оси, его размер по этой оси больше не зависит от содержимого там — ровно то, что нужно для запросов, и ровно то, что удивляет, когда inline-size-контейнер схлопывается, потому что ему никто не задал ширину. Всегда убеждайтесь, что контейнер получает inline-размер от раскладки (трек грида, flex-basis, явная ширина).
Несколько вещей, которые стоит усвоить, прежде чем сыпать @container повсюду:
- Контейнер запроса видит только своих потомков; соседи и предки для него невидимы.
- Имена — дешёвая страховка: безымянные контейнеры разрешаются к ближайшему предку нужного типа, а это при вложенности редко то, что вы имели в виду.
- Единицам
cqiи правилам@containerнужен установленный контейнер выше; без него единицы откатываются к маленькому вьюпорту, а правила просто никогда не срабатывают.
Выигрыш — та самая часть, что меняет подход к сборке: компоненты перестают тащить в себе предположения о странице. Карточка, которую я отдаю, отзывчива к тому, куда её положат, владелец раскладки решает про слоты, и две заботы перестают протекать друг в друга. Это разделение стоит куда больше, чем горстка брейкпоинтов, которую оно удаляет.