Контейнерные запросы
Адаптивность по ширине контейнера, а не по ширине окна. Тот же строй классов, та же шкала брейкпоинтов, разница в одну букву: -md смотрит на окно, -cmd — на ближайшего предка с .gr-cq.
Ядро + утилиты + компоненты
packages/core/scss/_container-queries.scss
Зачем
Оконный брейкпоинт отвечает на вопрос «какой ширины экран», а вёрстке
обычно нужен другой: «сколько места досталось этому блоку». Одна и та же
карточка стоит и в широкой ленте, и в узкой боковой колонке — окно при этом
не меняется, а раскладка должна.
container-type: inline-size включает инлайн-containment.
Ширина элемента перестаёт зависеть от его содержимого. У inline-block,
у float, у элемента с width: fit-content она схлопывается
в ноль, и содержимое уезжает наружу. Ставьте .gr-cq на блок, ширину
которого задаёт раскладка: на колонку сетки, на карточку, на body.
По той же причине библиотека намеренно не объявляет контейнером
body за вас. Иначе каждый безымянный @container на странице
разрешался бы к окну вместо того, чтобы просто не сработать, — тихий неверный
ответ вместо громкого отказа.
Объявление контейнера
| Класс | CSS | Назначение |
.gr-cq | container-type: inline-size
container-name: var(--gr-name, none) | Элемент становится контейнером запроса по инлайн-оси |
--gr-name | @property, inherits: false | Имя контейнера, если нужно обратиться к нему через голову ближайшего |
.gr-cq-page, .gr-cq-main, .gr-cq-aside, .gr-cq-card | container: page / inline-size и т.д. | Контейнер с именем классом — четыре слота раскладки как соглашение библиотеки (см. «Виджеты по гнезду») |
<div class="gr-cq">
<article class="gr-p-3 gr-p-5-cmd">…</article>
</div>
Суффикс -c<брейкпоинт>
Шкала — та же карта $gr-breakpoints, что и у оконных запросов.
Отдельной шкалы для контейнеров нет намеренно: она удвоила бы источник правды.
| Суффикс | Запрос | Читается как |
-csm | @container (width >= 640px) | от 640px ширины контейнера |
-cmd | @container (width >= 768px) | от 768px ширины контейнера |
-clg | @container (width >= 1024px) | от 1024px ширины контейнера |
-cxl | @container (width >= 1280px) | от 1280px ширины контейнера |
Шкала общая — и потому крупная. 768px ширины
контейнера — это много: карточка в трёхколоночной сетке
на экране 1440px имеет около 440px, в двухколоночной — около 680px.
Реально пересекаемый порог для блока внутри страницы — -csm
(640px); -clg и -cxl достаются разве что
основной колонке макета.
Отдельную, более мелкую шкалу для контейнеров библиотека не заводит
намеренно: это удвоило бы источник правды ровно там, где он был сведён
в одну карту. Нужен свой порог — он пишется одной строкой
своего CSS: @container (width >= 26rem) { … }.
Какие классы получили контейнерные варианты
| Модуль | Примеры |
| Явные сетки (ядро) | .gr-grid-1-csm … .gr-grid-12-cxl — число колонок по ширине гнезда |
| Flex (ядро) | .gr-flex-row-cmd, .gr-flex-col-csm, .gr-flex-wrap-clg — направление и перенос; выравниваний нет |
| Таблица (компоненты) | .gr-table-stack-cmd — карточки, пока гнездо уже порога |
Лента (griffinjs.css) | .gr-track-3-cmd — слайдов в ряд, .gr-track-stack-csm — столбик, пока гнездо уже порога |
| Spacing | .gr-p-4-cmd, .gr-px-8-clg, .gr-my-6-csm, .gr-mx-auto-cxl |
| Sizing | .gr-w-full-cmd, .gr-w-1/2-clg, .gr-h-auto-csm |
| Typography | .gr-text-lg-csm, .gr-font-bold-clg, .gr-text-center-csm, .gr-truncate-cxl |
| Visibility | .gr-hidden-cmd, .gr-block-clg, .gr-inline-block-csm |
Остальные модули контейнерных вариантов не получили.
Цвет, тень, граница, позиционирование, прозрачность и переходы от ширины
контейнера не зависят по смыслу, а каждый селектор — это байты в файле,
который скачивает каждый. Перечень модулей проверяется тестами
(packages/utils/test/utils.test.js для утилит,
packages/core/test/container.test.js для ядра), так что
расшириться молча он не может.
Логические отступы .gr-ms-*, .gr-me-*,
.gr-ps-*, .gr-pe-* несут оба вида суффиксов
наравне с осевыми — .gr-ms-4-md, .gr-ms-4-cmd:
после удаления физических односторонних классов именно они дают
адаптивный односторонний отступ.
Попробуйте: потяните за правый нижний угол
Ширина окна не меняется — меняется ширина контейнера
16 : 9
Карточка статьи
Узкий контейнер — картинка сверху, текст под ней.
От 640px ширины контейнера — картинка слева,
текст справа, и кегль заголовка на ступень крупнее.
Эта строка появляется только в широком контейнере
(.gr-hidden .gr-block-csm).
<div class="gr-cq">
<article data-gr-layout="a1-b1" data-gr-layout-csm="a1b2">
<div class="gr-area-a">…</div>
<div class="gr-area-b">
<h3 class="gr-text-base gr-text-lg-csm">…</h3>
</div>
</article>
</div>
Одна разметка, два контейнера
Ширина окна у обеих карточек ниже одна и та же. Различает их только ширина
колонки, в которой они стоят.
Слева 15rem, справа — остаток строки (порог 640px, нужно широкое окно)
16 : 9
Узкая колонка
Контейнер уже 640px — раскладка вертикальная.
16 : 9
Широкая колонка
От 640px контейнера — картинка слева, текст справа. Сузьте окно, и обе карточки станут вертикальными.
Виджеты по гнезду
Виджет магазина — подборка товаров, промо, таймер — монтируют в любую
позицию раскладки: в боковую колонку, в содержимое, в карточку товара.
Его вид должен зависеть от ширины гнезда, а не окна, и его CSS должен
уметь обратиться к гнезду по имени через голову ближайшего безымянного
.gr-cq. Имя классом, а не инлайновым
style="--gr-name: aside": инлайновый стиль в теме обычно
запрещён. Четыре имени — соглашение, чтобы тема и модули договаривались
об одних словах:
| Класс | Имя | Кто это |
.gr-cq-page | page | Страница целиком — обёртка макета |
.gr-cq-main | main | Содержимое — основная колонка |
.gr-cq-aside | aside | Боковая колонка |
.gr-cq-card | card | Карточка — товар, пост, панель |
<aside class="gr-cq-aside">
<section class="promo">…</section>
</aside>
/* CSS виджета: порог по гнезду, а не по окну */
@container aside (width < 40rem) { .promo { … } }
Пороги — те же, что у суффиксов: -csm / -cmd /
-clg / -cxl = 640 / 768 / 1024 / 1280 px ширины
гнезда. Так виджет собирается из готовых классов без своего CSS:
сетка .gr-grid-1 .gr-grid-3-cmd,
флекс .gr-flex-col .gr-flex-row-csm,
таблица
.gr-table-stack-cmd, лента
.gr-track-stack-csm .gr-track-3-cmd — столбик в узкой колонке,
три в ряд в широкой.
Своё имя по-прежнему через --gr-name; вариант
data-gr-cq="…" не сделан — attr() внутри
container-name не поддерживается ни одним движком.
Лента в двух гнёздах при одной ширине окна: сверху 14rem, снизу вся ширина колонки
Кресло «Дюна»
Торшер «Свет»
Полка «Норд»
Кресло «Дюна»
Торшер «Свет»
Полка «Норд»
Стол «Лофт»
Раскладки: data-gr-layout-c<брейкпоинт>
| Атрибут | Когда действует |
data-gr-layout | Базовая раскладка — ниже первого объявленного диапазона |
data-gr-layout-md | @media: от 768px ширины окна |
data-gr-layout-cmd | @container: от 768px ширины контейнера |
Контейнер должен быть предком, а не самим элементом.
@container ищет ближайшего предка с container-type
и никогда не спрашивает сам элемент: элемент не может измерять себя.
Поэтому .gr-cq ставится на обёртку, а data-gr-layout-cmd —
на то, что внутри неё. Рантайм не проставляет .gr-cq сам:
инлайн-containment, навешенный на чужую разметку без спроса, ломает страницу
молча и в неожиданном месте.
Оконная и контейнерная цепочки на одном элементе независимы — и не мешают
друг другу. Базовая раскладка при этом закрывается обеими сразу:
<div data-gr-layout="a1" data-gr-layout-md="a1b1" data-gr-layout-cmd="a2b1">
@media (width < 768px) {
@container (width < 768px) { .gr-l-хеш { /* a1 */ } }
}
@media (width >= 768px) { .gr-l-хеш { /* a1b1 */ } }
@container (width >= 768px) { .gr-l-хеш { /* a2b1 */ } }
Оставь база открытой по одной из осей — она накрыла бы диапазон другой,
и какая раскладка победит, решал бы порядок правил в таблице, а не ширина.
Когда оба диапазона совпали, побеждает контейнерный: его правило стоит
в таблице позже.
Вложенные контейнеры
Запрос без имени разрешается к ближайшему предку-контейнеру.
Вложили .gr-cq внутрь .gr-cq — всё внутри считается
от внутреннего, а внешний становится недосягаем. Это самая частая ошибка
с контейнерными запросами, и она не даёт ни ошибки в консоли, ни пустого стиля:
просто отвечает не тот элемент.
<section class="gr-cq"> <!-- внешний: 1200px -->
<div class="gr-cq"> <!-- внутренний: 300px -->
<p class="gr-text-lg-csm">…</p> <!-- считает 300px, а не 1200px -->
</div>
</section>
Чтобы обратиться к внешнему через голову внутреннего, дайте ему имя.
Имя приезжает свойством --gr-name, а не классом: имя контейнера —
это CSS-идентификатор, придуманный на стороне страницы, и статический файл
не может знать его список. Свойство общее, а не --gr-cq-name:
container-name, anchor-name и
view-transition-name берут один и тот же идентификатор,
и элемент вправе называться один раз на все роли.
<section class="gr-cq" style="--gr-name: page">
<div class="gr-cq">
<p class="wide-only">…</p>
</div>
</section>
/* своё правило страницы */
@container page (width >= 60rem) {
.wide-only { display: block; }
}
--gr-name зарегистрировано через @property
с inherits: false. Наследуйся оно — вложенный
.gr-cq перенял бы имя предка, и запрос
@container page нашёл бы внутренний контейнер вместо внешнего.
Та же причина, по которой ненаследуемы --gr-r и --gr-p
в каскаде скруглений.
Из SCSS
Обёртка запроса — миксин gr-container-media:
@use 'griffincss-core/scss/container-queries' as cq;
@include cq.gr-container-media(md) {
.my-card { padding: 2rem; }
}
// именованный контейнер
@include cq.gr-container-media(lg, page) {
.my-card { padding: 3rem; }
}
Компилтайм-раскладки принимают контейнерные варианты теми же параметрами
$csm … $cxl. Вывод побайтово совпадает с тем, что
генерирует рантайм, — это проверяется в
packages/core/test/generate.test.js:
@use 'griffincss-core/scss/grid-parser' as p;
@include p.gr-grid-layout('a1', $md: 'a1b1', $cmd: 'a2b1', $name: 'hero');
Поддержка
Контейнерные запросы — базовая возможность целевых браузеров с 2023 года
(Chrome 105+, Safari 16+, Firefox 110+). Развилки @supports
библиотека не делает: она и так требует :has() и
popover, которые новее, и удвоение вывода ради среды, которая
не поддерживается в остальном, ничего бы не купило.
Примеры использования
Контейнерные варианты пишутся один раз — а дальше виджет носит свою
адаптивность с собой, в какую бы клетку шаблона его ни поставили.
Два рецепта ниже — карточка, меняющая раскладку и словоохотливость
по месту, и KPI-плитка, меняющая плотность.
Карточка товара для любой клетки
Одна разметка на все места шаблона: в узкой клетке карточка вертикальна
и немногословна, в широкой — раскладывается картинкой влево
(data-gr-layout-csm), укрупняет заголовок
(.gr-text-lg-csm) и показывает описание, которого в узкой
версии нет вовсе: .gr-hidden .gr-block-csm — скрытие тоже
меряется контейнером. Обёртка-клетка получает .gr-cq,
сама карточка — нет: элемент не может измерять себя.
<div class="gr-cq"><!-- клетка шаблона любой ширины -->
<article class="gr-card gr-p-4" data-gr-layout="a1-b1" data-gr-layout-csm="a1b2">
<img class="gr-area-a gr-w-full gr-aspect-video gr-object-cover gr-radius-1" src="…" alt="">
<div class="gr-area-b">
<h3 class="gr-text-base gr-text-lg-csm">Кресло «Дюна»</h3>
<p class="gr-hidden gr-block-csm gr-text-sm gr-text-ink-secondary">
Описание видно только в широкой клетке.
</p>
<p class="gr-font-bold">12 400 ₽</p>
</div>
</article>
</div>
Клетки разной ширины — карточка одна (порог 640px контейнера, нужно широкое окно)
Фото
Кресло «Дюна»
Ротанг, съёмные подушки, каркас из бука — описание видно только в широкой клетке.
12 400 ₽
Фото
Кресло «Дюна»
Ротанг, съёмные подушки, каркас из бука — описание видно только в широкой клетке.
12 400 ₽
KPI-плитка: плотность по ширине места
Виджет статистики живёт и в тесном сайдбаре, и в широкой клетке
дашборда. Контейнерные варианты спейсинга и типографики подстраивают
плотность: в узком месте — .gr-p-3 и обычный кегль числа,
от 640px контейнера — просторный .gr-p-6-csm, крупное
число .gr-text-4xl-csm и центрирование
.gr-text-center-csm. Медиазапросами такое не решается:
ширина окна одна, а клетки — разные.
<div class="gr-cq">
<div class="gr-card gr-p-3 gr-p-6-csm gr-text-center-csm">
<p class="gr-text-xs gr-uppercase gr-text-ink-secondary">Выручка</p>
<p class="gr-text-2xl gr-text-4xl-csm gr-font-extrabold">1,2 млн ₽</p>
</div>
</div>
Одна плитка: сверху узкое место, ниже — широкое (порог 640px, нужно широкое окно)