CSS Container Queries: адаптивные компоненты

Краткое введение
Долгое время основным способом построения адаптивных интерфейсов были медиазапросы (media queries). Они отлично решают проблему адаптации под разные размеры экрана, но имеют ограничение: условие применимо только к размеру окна просмотра (viewport), а не к размеру отдельного элемента или его ближайшего контейнера.
Контейнерные запросы решают эту проблему — теперь можно писать правила, которые срабатывают, когда сам контейнер становится уже или выше заданного порога. Это особенно полезно при создании компонентной архитектуры (React, Vue, Web Components), где один и тот же компонент может попадать в разные контейнеры с разной шириной.
Код из примеров доступен в репозитории на GitHub и распространяется по MIT-лицензии.
Зачем использовать контейнерные запросы?
Рассмотрим практический пример. У нас есть страница с двумя колонками: основной контент и сайдбар. На широком экране карточки в обеих колонках выглядят рядом с картинкой; на узком экране карточка превращается в вертикальный блок (изображение сверху, контент снизу).

При использовании медиазапросов мы ориентируемся на ширину окна:
@media(max-width: 800px) {
.card {
flex-direction: column;
}
.card-header {
width: 100%;
}
}Если у вас простой макет — это работает. Но когда элементы вложены и один компонент оказывается внутри узкого сайдбара, медиазапрос по ширине окна не позволяет легко учесть локальные ограничения: сайдбар может быть достаточно узким даже на широком экране, и карточки в нём будут «съеживаться» независимо от окна.
Чтобы исправить это с помощью медиазапросов, придётся писать дополнительные селекторы, перечислять сочетания и поддерживать много правил:
.sidebar .card {
/* Make the sidebar card smaller */
}
@media(max-width: 800px) {
/* Style the page when the screen is narrower than 800px */
} Чем больше комбинаций контейнеров и компонентов — тем громоздче CSS. Контейнерные запросы избавляют от этой проблемы: компонент сам определяет своё поведение относительно контейнера, в котором он находится.
Как создать контейнерный запрос
- Сначала отметьте элемент как контейнер при помощи свойства container-type. Например, сделаем контейнером main и .sidebar:
main, .sidebar {
container-type: inline-size
} - Теперь внутри можно писать @container — он срабатывает, когда сам контейнер достигает указанных размеров:
@container(max-width: 500px) {
.card {
flex-direction: column;
}
.card-header {
width: 100%;
}
} В отличие от @media, @container проверяет размеры именно контейнера, а не viewport. На практике это означает, что одна и та же .card может вести себя по-разному в main и в sidebar в зависимости от локальной ширины контейнера.

На изображении видно: в сайдбаре карточка стала вертикальной, потому что ширина сайдбара меньше 500px; в основном контенте карточка осталась горизонтальной.
Важно: после добавления container-type сразу визуальных изменений не будет — вы просто подключаете возможность реагировать на размеры контейнера.
Именование контейнеров и выбор специфичного контейнера
По умолчанию @container ищет ближайший объявленный контейнер вверх по дереву DOM. Если вы объявили несколько контейнеров (body, main, aside), то ближайший к целевому элементу победит.
Если нужно ориентироваться не на ближайший контейнер, а на какой-то конкретный — задайте имя контейнера через container-name и укажите его в @container:
body {
container-type: inline-size;
container-name: body;
}
@container body (max-width: 1000px){
/* CSS rules that target the body container */
} Это полезно, когда требуется централизованно контролировать поведение вложенных компонентов относительно глобального контейнера.
Контейнерные единицы (container units)
Контейнерные единицы похожи на viewport-единицы (vw/vh), но привязаны к размеру контейнера: cqw (container query width) и cqh (container query height). Примеры использования:
.card {
font-size: calc(1rem + 0.5cqw);
}Эти единицы позволяют делать масштабирование шрифтов и размеров на основе локального контекста, а не окна браузера.
Пример: адаптивная карточка с запасом по доступности
Ниже пример карточки, которая меняет порядок элементов и размеры шрифта в зависимости от ширины контейнера.
/* Объявляем контейнер для секции */
.section {
container-type: inline-size;
}
/* По умолчанию карточка горизонтальная */
.card {
display: flex;
gap: 1rem;
align-items: flex-start;
}
.card-header {
width: 40%;
}
@container(max-width: 480px) {
.card {
flex-direction: column;
}
.card-header {
width: 100%;
}
.card {
font-size: 1.05rem; /* чуть крупнее для узкого контейнера */
}
}Этот подход даёт предсказуемое поведение компонента независимо от того, где он размещён.
Когда контейнерные запросы не подходят
Важно понимать ограничения и случаи, когда container queries не решают задачу:
- Если вам нужно реагировать на размеры самого окна браузера (например, управление поведением глобальной сетки), то медиазапросы остаются необходимыми.
- Если приложение динамически меняет размер контейнера через JavaScript и вы рассчитываете на синхронное поведение старых браузеров — учтите, что поддержка устаревших движков ограничена.
- Очень глубокая вложенность и сложные правила именования контейнеров могут привести к путанице; разумнее держать архитектуру контейнеров простой.
Альтернативы и дополняющие техники
- Media queries — по-прежнему актуальны для глобальных breakpoint-стратегий.
- ResizeObserver + JavaScript — гибко, но сложнее и может ухудшать производительность при неправильном использовании.
- CSS-переменные + медиазапросы — удобны для централизованного управления стилями и миграции на контейнерные запросы поэтапно.
Пример простого наблюдателя, если нужно триггерить JS-логики по изменению размера контейнера:
const ro = new ResizeObserver(entries => {
for (let entry of entries) {
// entry.contentRect.width => текущая ширина контейнера
// Логика обновления классов или переменных CSS
}
});
ro.observe(document.querySelector('.section'));Используйте ResizeObserver только если CSS не может решить задачу: JS-обход работает медленнее и требует тестирования производительности.
Ментальные модели и эвристики при проектировании
- Думайте о «локальном окне»: при работе с компонентом представляйте, что у него есть собственное окно просмотра — его контейнер.
- Разделяйте правила: глобальные точки останова — через @media, локальные — через @container.
- Называйте контейнеры осмысленно (например, app-shell, card-frame), если пользуетесь container-name.
- Поддерживайте обратную совместимость: сначала добавляйте container-type, потом постепенно переводите поведение из @media в @container.
Миграция существующих компонентов — мини-методология
- Инвентаризация: найдите компоненты, которые меняют поведение в зависимости от ширины контекста (например, карточки в сайдбаре).
- Объявите соответствующие контейнеры: добавьте container-type к элементам-обёрткам.
- Переместите локальные медиазапросы в @container внутри стилей компонента.
- Тестирование: проверьте компонент в разных контейнерах и на разных устройствах.
- Рефактор: удалите избыточные селекторы, когда поведение стабильно.
Критерии приёмки
- Компонент корректно меняет макет при достижении порогов контейнера.
- Нет визуальных регрессий на страницах, где компонент используется в нескольких местах.
- Производительность на странице в норме: отсутствие частых перерисов или лагов.
- Покрытие ручных тестов для ключевых комбинаций размеров контейнеров.
Роли и чек-листы
Разделю на две роли — разработчик и дизайнер.
Разработчик:
- Добавил container-type к корневому элементу контейнера.
- Перенёс локальные медиазапросы в @container, где это имеет смысл.
- Проверил поддержку браузеров и добавил полифилы/фоллбэки при необходимости.
- Прогнал тесты производительности и визуальные тесты.
Дизайнер:
- Обозначил желаемые точки изменения макета в терминах локального контейнера.
- Согласовал, какие элементы должны адаптироваться независимо от окна.
- Предоставил макеты для разных ширин контейнера (например: >800px, 500–800px, <500px).
Совместимость и советы по миграции
На момент написания: современные версии Chromium, Firefox и Safari поддерживают контейнерные запросы. Однако старые браузеры могут не поддерживать их. Рекомендации:
- Для критичных интерфейсов используйте progressive enhancement: добавьте контейнерные запросы как улучшение, а базовый стиль делайте доступным и без них.
- Тестируйте в реальных браузерах и включайте fallback-правила (например, медиазапросы) для старых окружений.
- Если нужна совместимость с IE11/Edge Legacy — придётся использовать JavaScript-обходы.
Примеры плохих пар и сценарии отказа
- Не стоит объявлять слишком много вложенных контейнеров с разными именами без явной нужды — это усложняет отладку.
- Если компонент зависит от глобального расположения (например, full-bleed баннеры), лучше оставить медиазапросы.
- В высокочастотных анимациях или при частом изменении размеров контейнера проверяйте производительность: избыток пересчётов может снизить FPS.
Небольшая галерея исключительных случаев
- Компонент внутри iFrame — контейнерные запросы работают относительно контейнера в фрейме, но учтите контекст iframe.
- Компонент в layout-системе с CSS Grid: container queries готовы к взаимодействию, но порядок и placement grid-ячейки могут усложнить логику изменений.
1-строчная глоссарий
- container-type: CSS-свойство, объявляющее элемент как контейнер.
- @container: директива, похожая на @media, но ориентированная на контейнер.
- cqw / cqh: контейнерные единицы ширины и высоты.
Решение «что использовать?» — простое дерево принятия решений
flowchart TD
A[Нужно адаптировать элемент?] --> B{Зависит от всего окна?}
B -->|Да| C[Используйте @media]
B -->|Нет| D{Можно ли объявить контейнер?}
D -->|Да| E[Добавьте container-type и @container]
D -->|Нет| F[Используйте ResizeObserver или медиазапросы как fallback]Риски и способы их снижения
- Риск: отсутствие поддержки в целевых браузерах. Смягчение: progressive enhancement, fallback-медиазапросы, тестирование.
- Риск: логическая запутанность при множественных именованных контейнерах. Смягчение: документировать архитектуру контейнеров и выбрать единый подход к именованию.
- Риск: ухудшение производительности. Смягчение: избегать частых JS-пересчётов, оптимизировать CSS и тестировать реальную нагрузку.
Заключение
Контейнерные запросы изменяют парадигму адаптивного дизайна: вместо единого глобального окна вы получаете «локальные окна» для каждого компонента. Это делает компоненты более устойчивыми, переносимыми и предсказуемыми.
Важно сочетать контейнерные запросы с медиазапросами там, где это уместно, и соблюдать принципы progressive enhancement для совместимости. Переход на контейнерные запросы стоит делать поэтапно: объявляйте контейнеры, переносите локальные правила и тестируйте.
Короткая памятка — что делать сейчас:
- Добавьте container-type к контейнерам, где компоненты чувствуют себя по-разному.
- Перенесите локальные точки изменения в @container.
- Тестируйте компоненты в разных контекстах и браузерах.
Ресурсы и дальнейшее чтение
- Официальная документация по спецификации CSS Container Queries (поиск по MDN/Web Platform).
- Примеры и репозиторий с исходным кодом в GitHub (MIT).
Важно: переходите на контейнерные запросы постепенно и держите обратную совместимость для пользователей старых браузеров.
Примечание: если нужно, могу подготовить набор визуальных тестов и чек-листов для автоматической проверки поведения компонентов в разных контейнерах.