Контейнерные запросы против медиа-запросов: решения для страниц и компонентов
Узнайте, когда следует использовать медиа-запрос, а когда более подходит контейнерный запрос, с примерами в виде карточек, панелей управления и единиц измерения, которые позволяют адаптировать повторно используемые компоненты.
Источники: Container Queries vs Media Queries: How Modern CSS Changes Responsive Design
Большинство решений для создания адаптивного дизайна в CSS по-прежнему зависят от одного параметра — ширины окна браузера. Это хорошо работает для всей страницы в целом, но не справляется с повторно используемыми компонентами, поскольку карточка, отображаемая в боковой панели, и та же карточка, отображаемая в широкой основной колонке, видят точно такой же обзорный пространство, но имеют совершенно разное количество свободного места. В этом руководстве показано, как контейнерные запросы устраняют эту проблему, где медиа-запросы остаются более подходящим инструментом, и как сочетать оба подхода с плавными единицами измерения для создания адаптивной системы, которую можно логически осмыслить.
Классическое правило, основанное на обзорном пространстве, выглядит так: как только ширина окна опускается ниже критического значения, содержимое карточки выстраивается вертикально.
@media (max-width: 768px) {
.card {
flex-direction: column;
}
}
Ничего здесь не сломано. Проблема кроется в концепции: правило определяет ширину окна, но никогда не учитывает ширину карточки. В небольшой демонстрации эти значения совпадают, но в приложении, полном сеток, боковых панелей и других элементов, они различаются, и именно в этой разнице появляются ошибки макета. Инструменты контейнерных запросов позволяют компоненту спрашивать «Сколько места у меня на самом деле?», а не «Какова ширина экрана?».
Почему точка разрыва обзорного окна вводит в заблуждение повторно используемую карточку
Возьмём простую карточку продукта: изображение рядом с блоком текста, содержащим заголовок, краткое описание и призыв к действию. Маркировка здесь разделена на два фрагмента, но на самом деле это один элемент.
<article class="product-card">
<img src="product.jpg" alt="Product">
<div class="product-info">
<h2>Wireless Headphones</h2>
<p>Noise cancellation with 30-hour battery life.</p>
<button>Buy now</button>
</div>
</article>
На первом этапе стилизации обычно с помощью flexbox размещают изображение и текст рядом друг с другом, а затем добавляют точку разрыва обзорного окна, чтобы на маленьких экранах они располагались один под другим.
.product-card {
display: flex;
gap: 1rem;
}
@media (max-width: 768px) {
.product-card {
flex-direction: column;
}
}
Всё кажется в порядке, пока карточка не используется повторно. На широкой рабочей области экрана есть достаточно места в горизонтальном направлении, и расположение карточек рядом друг с другом выглядит корректно.
Desktop page
┌───────────────────────────────────────────┐
│ │
│ Product Card │
│ ┌──────────┐ ┌─────────────────────┐ │
│ │ Image │ │ Product information │ │
│ └──────────┘ └─────────────────────┘ │
│ │
└───────────────────────────────────────────┘
Если поместить ту же самую карточку в узкую боковую панель, ей потребуется вертикальное расположение, хотя размер окна по-прежнему соответствует размеру рабочего стола.
┌───────────────┐
│ │
│ Sidebar │
│ │
│ ┌───────────┐ │
│ │ Image │ │
│ ├───────────┤ │
│ │ Info │ │
│ └───────────┘ │
│ │
└───────────────┘
Если разместить несколько таких карточек в одном ряду на панели управления, каждая из них будет занимать разную долю ширины в зависимости от своей колонки.
┌───────────────────────────────────────────┐
│ ┌─────────────┐ ┌──────────────────────┐ │
│ │ Card │ │ Card │ │
│ └─────────────┘ └──────────────────────┘ │
└───────────────────────────────────────────┘
Три варианта размещения, три разных размера ширины — при этом область отображения остается неизменной. Правила, основанные на размерах окна, не могут различать эти варианты, поэтому карточка в боковой панели остается тесной.
Чем на самом деле хороши медиа-запросы
Медиа-запросы оценивают свойства среды, в которой отображается страница: размеры области просмотра, ориентация, цветовая схема, тип указателя, настройки сокращения анимаций и так далее. Известным примером является запрос на определение ширины.
@media (max-width: 768px) {
...
}
Поток информации прост: браузер измеряет область просмотра, проверяется соответствие запросу, и макет реагирует на это.
Viewport
↓
Media Query
↓
Layout changes
Именно поэтому медиа-запросы являются подходящим инструментом для принятия решений, которые действительно касаются всей страницы в целом. Типичным примером является двухколоночная структура с фиксированной по ширине боковой панелью:
.page-layout {
display: grid;
grid-template-columns: 1fr 300px;
}
При ширине окна, меньшей определенного значения, структура преобразуется в одноколоночную:
@media (max-width: 900px) {
.page-layout {
grid-template-columns: 1fr;
}
}
Это правильное использование медиа-запроса: имеется ли на странице место для боковой панели, действительно зависит от характеристик области просмотра. Проблемы возникают, когда тот же механизм используется для определения способа расположения внутренних элементов отдельного компонента.
Как работают запросы к контейнерам
Запрос к контейнеру позволяет элементу реагировать на размер предка, который был явно отмечен как контейнер для запросов. Для настройки этого требуется два шага. Сначала необходимо объявить контейнер:
.card-container {
container-type: inline-size;
}
Значение inline-size сообщает браузеру, что потомки могут запрашивать линейный размер контейнера, то есть его ширину в режимах горизонтального форматирования. Затем нужно написать правило для этого контейнера:
@container (max-width: 500px) {
.product-card {
flex-direction: column;
}
}
Теперь информация поступает от контейнера, а не от окна:
Container
↓
Container Query
↓
Component layout
Карточка больше не зависит от того, использует ли посетитель телефон или большой монитор; она реагирует на пространство, которое предоставляет ей родительский элемент.
Один момент, который сбивает людей с толку
Запрос к контейнеру форматирует потомков контейнера, но никогда не сам контейнер. Именно поэтому в примерах всегда сочетается обёртка (.card-container, .card-wrapper) с внутренним элементом (.product-card). Если вы установите атрибут container-type на элемент .product-card, а затем выполните запрос для изменения его собственного значения flex-direction, ничего не произойдёт. Держите обёртку и компонент как отдельные элементы.
Второй момент: модус управления размерами inline-size означает, что ширина контейнера больше не может быть определена на основе его содержимого. Контейнер, являющийся блочным элементом в обычном лейауте, работает нормально, поскольку его ширина задаётся родителем. Контейнеры, которые используют метод уменьшения размера до подходящего значения, такие как элементы типа inline-block или абсолютно позиционированные элементы без явно указанной ширины, могут сократиться до нулевой ширины после применения данного модуса.
Ширина области отображения против ширины компонента
Эта разница становится очевидной на панели управления с широкой основной колонкой и более узкой боковой колонкой:
.dashboard {
display: grid;
grid-template-columns: 2fr 1fr;
gap: 1rem;
}
Предположим окно шириной 1440 пикселей. Правило, основанное на области отображения, для карточки будет выглядеть так:
@media (max-width: 768px) {
.card {
flex-direction: column;
}
}
В таком окне это правило никогда не срабатывает. Однако карточка, находящаяся в более узкой колонке, может иметь ширину всего около 350 пикселей. Медиа-запрос учитывает только одно значение:
Viewport = 1440px
Карточка находится вместе с другой:
Component width = 350px
Только второе число отражает реальное состояние карточки, и только запрос к контейнеру может на него отреагировать.
Минимальная настройка запроса к контейнеру
Реализация простая. Обозначьте обертку как контейнер:
.card-wrapper {
container-type: inline-size;
}
При ширине контейнера не менее 600 пикселей используйте двухколоночную сетку для карточки:
@container (min-width: 600px) {
.product-card {
display: grid;
grid-template-columns: 200px 1fr;
}
}
В противном случае перейдите на простую блочную компоновку:
@container (max-width: 599px) {
.product-card {
display: block;
}
}
Для маркировки требуется только элемент-обертка вокруг карточки:
<div class="card-wrapper">
<article class="product-card">
...
</article>
</div>
Теперь макет карточки определяется её контейнером, поэтому перемещение обёртки в другой столбец автоматически формирует правильный макет. Обратите внимание, что два правила используют min-width: 600px и max-width: 599px; при дробных значениях ширины контейнер может находиться между этими значениями. Более современная синтаксис диапазонов @container (width >= 600px) и @container (width < 600px) исключает такую промежутковую зону, поэтому её следует предпочитать в новом коде.
Почему это соответствует архитектуре на основе компонентов
Запросы к контейнерам имеют большее значение, чем обычные новые функции CSS, поскольку современные фронтенды состоят из повторно используемых компонентов — будь то элементы фреймворков в React, Vue или Angular, Web Components без фреймворка или базовые элементы общей системы дизайна. Ни один из этих компонентов не должен знать, в какой области страницы он окажется. Возьмем, к примеру, элемент user-card. Он может отображаться в основной области контента:
<main>
<user-card></user-card>
</main>
Он также может отображаться в блоке справа:
<aside>
<user-card></user-card>
</aside>
Или его можно повторять в сетке дашборда:
<div class="dashboard-grid">
<user-card></user-card>
<user-card></user-card>
<user-card></user-card>
</div>
Жесткое задание значения порога просмотра в этом компоненте подразумевает наличие предположения, которое невозможно проверить:
@media (max-width: 768px) {
...
}
Число 768 ничего не говорит о реальном контексте компонента. Запросы контейнера предлагают более четкие правила: при наличии определенного пространства использовать именно такой макет. Если ваши компоненты являются независимыми строительными блоками, обсуждение того, где должны находиться границы фреймворка в многократно используемой UI-стеке, становится полезным дополнением к этой идее.
Позволение карточке профиля адаптироваться к окружающей среде
Вот компактная карточка профиля, в которой аватар размещается рядом с именем и должностью:
.profile-card {
display: flex;
gap: 1rem;
align-items: center;
}
Ее обертка превращается в контейнер для запросов:
.profile-container {
container-type: inline-size;
}
Когда ширина этого контейнера составляет 400 пикселей или меньше, карточка складывает свой контент и центрирует его:
@container (max-width: 400px) {
.profile-card {
flex-direction: column;
text-align: center;
}
}
В просторном контейнере аватар находится рядом с текстом:
┌──────────────────────────────────────┐
│ [Avatar] John Smith │
│ Senior Developer │
└──────────────────────────────────────┘
В критической ситуации тот же компонент перестраивается вертикально:
┌──────────────────┐
│ │
│ [Avatar] │
│ │
│ John Smith │
│ Senior Developer │
│ │
└──────────────────┘
Нет наблюдателя за изменением размера, нет второй «компактной» версии, нет класса, управляемого родительской страницей.
Условия медиа всё ещё нужны
Условия контейнера не заменяют условия медиа; они отвечают на другой вопрос. Вот краткий способ классификации распространённых решений:
- Скрытие основного навигационного меню: условие медиа.
- Удаление боковой панели на уровне страницы: условие медиа.
- Переключение карточки между горизонтальным и вертикальным форматом: условие контейнера.
- Адаптация повторно используемого компонента к его местоположению: условие контейнера.
- Настройка типографики на всем сайте: условие медиа.
- Позволение виджету панели управления реагировать на ширину своей колонки: условие контейнера.
- Переключение всей оболочки приложения на другой формат: условие медиа.
Ментальная модель: глобальные решения и решения компонентов
Самый ясный способ разделения задач — это отделить то, что определяет страница, от того, что определяет каждый компонент.
Запросы к медиа-среде касаются области отображения
Вопрос, стоящий за запросом к медиа-среде, заключается в следующем:
"What is the viewport doing?"
Они подходят для:
- Структуры страницы
- Навигации
- Глобального расстояния между элементами
- Глобальной типографики
- Крупных изменений лейаута
Запросы к контейнеру касаются окружения компонента
Вопрос, стоящий за запросом к контейнеру, заключается в следующем:
"What is my component's environment doing?"
Они подходят для:
- Карточек
- Виджетов
- Панелей
- Переиспользуемых компонентов интерфейса
- Модулей панели управления
- Компонентов системы дизайна
Размерирование с использованием единиц запросов к контейнеру
Запросы к контейнерам — это не только условные блоки. CSS также определяет единицы длины относительно ближайшего контейнера-запроса, поэтому значения могут изменяться плавно, а не скачками при достижении определенного порога:
.title {
font-size: 5cqi;
}
Здесь cqi означает один процент внутреннего размера контейнера, так что 5cqi — это пять процентов его ширины. Полный список единиц:
cqw → container width
cqh → container height
cqi → container inline size
cqb → container block size
cqmin → smaller container dimension
cqmax → larger container dimension
Часто используется паттерн, при котором единица контейнера оборачивается в функцию clamp(), чтобы значение масштабировалось вместе с контейнером, но всегда оставалось в разумных пределах:
.card-title {
font-size: clamp(1rem, 4cqi, 2rem);
}
Теперь заголовок масштабируется в зависимости от пространства компонента, а не окна, что невозможно выразить с помощью единиц обзорной области, таких как vw. Есть два важных момента. Если у элемента нет родительского контейнера для применения запросов, эти единицы переходят на более мелкие единицы обзорной области, поэтому значение всё равно будет вычислено, но не то, которое вы ожидали. Кроме того, использование параметров минимума и максимума для единиц rem внутри функции clamp(), как показано выше, важно с точки зрения доступности: размер шрифта, определяемый исключительно шириной контейнера, не реагирует на настройки размера текста пользователя.
Называние контейнеров в крупных приложениях
Для небольшой страницы достаточно анонимного контейнера:
.container {
container-type: inline-size;
}
Когда контейнеры вложены друг в друга, правило @container срабатывает с самым близким родительским контейнером, который может не соответствовать ожидаемому. Сокращение container позволяет присвоить контейнеру имя вместе с его типом:
.dashboard {
container: dashboard / inline-size;
}
Затем правила могут напрямую указывать на этот контейнер:
@container dashboard (min-width: 700px) {
.widget {
grid-template-columns: 1fr 1fr;
}
}
Сравните безымянное условие:
@container (min-width: 700px)
с именованным:
@container dashboard (min-width: 700px)
Именованная версия четко указывает на свою цель и продолжает работать даже если позже кто-то добавит ещё один контейнер между панелью управления и виджетом.
Тщательно выбирайте границы контейнеров
Запросы к контейнерам мощны, но это не повод добавлять такое объявление к каждому элементу:
container-type: inline-size;
Каждый контейнер применяет правила макетирования, стилей и определения размера элемента внутри него, что влияет на то, как может быть задан размер такого элемента. Поэтому контейнеры должны обозначать четкие границы компонентов, а не располагаться везде. Рассмотрим панель управления, содержащую внутри себя виджеты:
<section class="dashboard">
<article class="widget">
<div class="widget-content">
...
</div>
</article>
</section>
Разумным решением будет сделать панель управления именованным контейнером:
.dashboard {
container: dashboard / inline-size;
}
и позволить каждому виджету реагировать на нее. Правило заключается не в том, чтобы «использовать контейнерные запросы повсюду», а в том, чтобы использовать их там, где поведение зависит от доступного пространства».
Замена точек разрыва на основе размеров экрана на точки разрыва, определяемые содержимым
Во многих таблицах стилей присутствует иерархия точек разрыва для обзорной области в таком виде:
@media (max-width: 1200px) {
...
}
@media (max-width: 1024px) {
...
}@media (max-width: 900px) {
...
}@media (max-width: 768px) {
...
}@media (max-width: 640px) {
...
}@media (max-width: 480px) {
...
}
Иногда такая структура оправдана. Часто же она свидетельствует о том, что компоненты были спроектированы с учетом ширины устройства, а не их собственного содержимого. Запросы к контейнерам побуждают задавать другой вопрос. Вместо того чтобы спрашивать «какой точка разрыва обычно используется командой?», стоит спросить «при какой ширине этот компонент перестает выглядеть корректно?» Ответ позволяет определить точку разрыва, основанную на содержимом:
@container (max-width: 420px) {
.card {
flex-direction: column;
}
}
Цифра 420 пикселей не соответствует конкретному телефону; это ширина, при которой у карточки заканчивается место. Точки разрыва, выбранные таким образом, сохраняются при изменениях макета и появлении новых устройств.
Проектирование компонентов, не зависящих от расположения
Запросы к контейнерам также помогают командам создавать по-настоящему независимые компоненты, поведение которых предсказуемо в любом месте их отображения. Представьте группу кнопок из системы дизайна. Она может находиться внутри формы на странице:
Page
└── Form
└── Button Group
внутри карточки на панели управления:
Dashboard
└── Card
└── Button Group
или внутри карточки в боковой панели:
Sidebar
└── Card
└── Button Group
Видимая область одинакова во всех трех случаях; пространство — нет. Благодаря запросам к контейнеру группа адаптируется, не зная ничего о структуре над ней.
Паттерн адаптивной карточки с возможностью повторного использования
Вот полный паттерн, который можно сразу применить. Маркировка оборачивает карточку в элемент-контейнер; HTML также показан в двух последовательных частях.
<div class="card-container">
<article class="card">
<img src="image.jpg" alt="">
<div class="card-content">
<h2>Performance Dashboard</h2>
<p>
Monitor application performance and infrastructure metrics.
</p>
<a href="#">View dashboard</a>
</div>
</article>
</div>
Обертка объявляется как контейнер:
.card-container {
container-type: inline-size;
}
Карточка изначально представляет собой одноколоночную сетку с изображением соотношения 16:9. Как только её контейнер достигает размера 600 пикселей, она переходит на режим с фиксированной колонкой изображения рядом с содержимым и приводит изображение к квадратному формату. Правила написаны без переносов строк, что является допустимым в CSS, но требует переформатирования при вставке в кодовую базу.
.card {
display: grid;
gap: 1rem;
}.card img {
width: 100%;
aspect-ratio: 16 / 9;
object-fit: cover;
}@container (min-width: 600px) {
.card {
grid-template-columns: 200px 1fr;
align-items: center;
} .card img {
aspect-ratio: 1;
}
}
Обратите внимание, чего нет в этом стилевом листе:
@media (max-width: 768px)
Карточка никогда не учитывает область отображения. Все её решения основаны на своем контейнере.
Сочетание запросов к контейнеру с автоматически подстраивающейся сеткой
Запросы к контейнеру особенно хорошо сочетаются с сеткой, которая сама определяет количество колонок:
.dashboard {
display: grid;
grid-template-columns:
repeat(auto-fit, minmax(280px, 1fr));
gap: 1rem;
}
С помощью repeat(auto-fit, minmax(280px, 1fr)) браузер формирует столько колонок шириной не менее 280px, сколько позволяет строка, и растягивает их для заполнения оставшегося пространства. Каждый виджет тогда получает свой собственный контейнер:
.widget-container {
container-type: inline-size;
}
и переходит на внутреннюю двухколонную компоновку, когда его слот достаточно широк:
@container (min-width: 500px) {
.widget {
grid-template-columns: 1fr 1fr;
}
}
Панель управления адаптируется к самым разным ширинам без использования точек разрыва обзора: сетка определяет, сколько пространства получит каждый виджет, а каждый виджет решает, как им воспользоваться.
Адаптивная компоновка также влияет на удобство использования
То, что элементы помещаются на экране, не означает, что их легко использовать. Ряд кнопок, который технически помещается на узкой карточке, может оставаться тесным и затруднять нажатие. С помощью контейнерных запросов действия могут быть выстроены одно под другим и растянуты до полной ширины, когда карточка узкая (правило расположено в двух фрагментах кода):
@container (max-width: 350px) {
.actions {
flex-direction: column;
}
.actions button {
width: 100%;
}
}
Кнопки, занимающие всю ширину экрана, обеспечивают более крупные зоны нажатия и снижают вероятность случайных нажатий. То же самое применимо к тексту: его размер должен изменяться в пределах допустимых значений по мере изменения ширины компонента.
.card-title {
font-size: clamp(1rem, 4cqi, 1.75rem);
}
Конкретные значения имеют меньшее значение, чем принцип, лежащий в их основе: реактивное поведение должно отражать условия, в которых фактически используется компонент, а не предположения, основанные на размере окна.
Когда медиа-запросы остаются правильным выбором
Контейнерные запросы не всегда являются лучшим вариантом. Глобальная навигация — яркий пример случая, когда необходим медиа-запрос, поскольку скрытие полного меню и отображение кнопки меню — это решения, связанные с структурой страницы (это правило также делится на два пункта):
@media (max-width: 900px) {
.navigation {
display: none;
}
.menu-button {
display: block;
}
}
Предпочтения пользователя относятся к совершенно другой категории. Снижение скорости анимаций для тех, кто попросил операционную систему уменьшить количество анимаций, не связано ни с чем, что касается контейнеров:
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms;
animation-iteration-count: 1;
}
}
Это настройка среды, которую могут читать только медиа-запросы. Цель не в устранении медиа-запросов, а лишь в том, чтобы не использовать их для принятия решений относительно компонентов.
Трехуровневый подход к адаптивному CSS
Для приложения, построенного на компонентах, полезно рассматривать адаптивное стилизование в трех уровнях.
Уровень 1: глобальная адаптивность с помощью медиа-запросов
Медиа-запросы отвечают за вопросы, касающиеся всей страницы:
Page layout
Navigation
Global typography
Major structural changes
Application shell
Уровень 2: адаптивность компонентов с помощью запросов к контейнерам
Запросы к контейнерам отвечают за внутреннюю структуру самостоятельных элементов:
Cards
Widgets
Panels
Reusable components
Design-system components
Dashboard modules
Уровень 3: плавное поведение с использованием функций и единиц измерения
Там, где не требуется жесткий разрыв точек, позвольте значениям непрерывно меняться с помощью современных функций и единиц измерения:
clamp()
min()
max()
minmax()
cqi
cqw
Например, заголовок может расти вместе со своим контейнером в пределах двух фиксированных значений:
.heading {
font-size: clamp(1.5rem, 3cqi, 3rem);
}
Вместе эти уровни образуют систему, а не просто набор временных решений для разрывов точек.
Настоящее изменение заключается в том, кто принимает решения
Хочется сводить запросы к контейнерам к формулировке «запросы к медиа-содержимому для элементов». Это технически верно, но не отражает суть. Изменяется модель ответственности. В традиционном адаптивном CSS страница фактически определяет, как будет вести себя каждый компонент:
Viewport
↓
Media Query
↓
Component
С использованием запросов к контейнерам каждый компонент получает указания от своего собственного контейнера:
Component Container
↓
Container Query
↓
Component
Компоненты, имеющие собственные правила адаптивности, легче повторно использовать, перемещать, тестировать изолированно и обслуживать. Именно такой переход ответственности является долгосрочной выгодой.
Один вопрос для решения каждого случая
Когда вы не уверены, какой инструмент использовать, спросите себя, касается ли решение по макету всей страницы или конкретного компонента. Если речь идет о странице, используйте запросы к медиа-среде. Если речь идет о пространстве, выделенном для компонента, используйте запросы к контейнеру. Применение к типичным случаям:
Should the sidebar disappear?
↓
Media Query
Should this card switch from horizontal
to vertical?
↓
Container Query
Should the navigation collapse?
↓
Media Query
Should this reusable widget change
its internal layout?
↓
Container Query
Основные выводы
- Запросы к медиа-среде описывают область отображения и пользовательскую среду, поэтому они подходят для структуры страницы, навигации, глобальной типографики и настроек, таких как снижение скорости анимаций.
- Запросы к контейнеру описывают пространство, выделенное для компонента, поэтому они подходят для карточек, виджетов, панелей и компонентов дизайн-системы.
cqi и cqw, используемые вместе с функцией clamp() и ограничениями в виде rem, позволяют достигать гибкого форматирования без ущерба для настроек размера текста пользователя.