Статья · Личный блог

Контейнерные запросы: компонент реагирует на свой слот, а не на экран

Переход от медиазапросов к @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 по его собственному размеру.

Решение структурное: контейнер — это обёртка, а то, что ты перестилизуешь, — потомок.

В файловом менеджере, над которым я работал, карточка превью жила в трёх разных слотах — сайдбар, модалка, ячейка сетки — с одинаковой разметкой. Контейнером владела обёртка; карточка просто реагировала. Один компонент, три контекста, ноль условного рендеринга.

Чего это стоит и где кусается

Size containment не бесплатен концептуально. Как только элемент стал size-контейнером по оси, его размер по этой оси больше не зависит от содержимого там — ровно то, что нужно для запросов, и ровно то, что удивляет, когда inline-size-контейнер схлопывается, потому что ему никто не задал ширину. Всегда убеждайтесь, что контейнер получает inline-размер от раскладки (трек грида, flex-basis, явная ширина).

Несколько вещей, которые стоит усвоить, прежде чем сыпать @container повсюду:

Выигрыш — та самая часть, что меняет подход к сборке: компоненты перестают тащить в себе предположения о странице. Карточка, которую я отдаю, отзывчива к тому, куда её положат, владелец раскладки решает про слоты, и две заботы перестают протекать друг в друга. Это разделение стоит куда больше, чем горстка брейкпоинтов, которую оно удаляет.