Адаптивность без экрана: @container против @media
Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.
Адаптивная вёрстка подстраивается под место: в широкой колонке карточка товара ставит картинку слева от текста, а в узкой боковой — над ним. Много лет такое писали через @media: «если окно шире 600 пикселей, то…». Только карточка не знает, какое у читателя окно и в какую колонку её положат, — ей важно одно: сколько места досталось именно ей. Отсюда и главная несправедливость @media: двум одинаковым карточкам на одной странице оно отвечает одинаково, даже если одна стоит в широкой колонке, а другая в узкой. Разберём, почему так выходит, и спросим о ширине саму коробку — @container, единицы cqi и clamp().
Правило @media — обёртка: в скобках условие, а в фигурных скобках обычные правила. Пока условие выполняется, правила внутри действуют; перестало — браузер их не применяет. Чаще всего условие спрашивает о размере окна: @media (max-width: 600px) { … } читается как «окно не шире 600 пикселей», а (min-width: 600px) — «не уже 600». О высоте спрашивают так же, через max-height и min-height.
Окно здесь — не монитор, а область, в которой браузер раскладывает документ. У вкладки это её видимая часть. А у примера учебника свой документ: его страница стоит в рамке, и для её стилей окно — сама рамка. Высота рамки у всех читателей одна, 140 пикселей. В примере заголовок чёрный, а при окне не выше 200 пикселей — красный. Прогноз: каким цветом окажется заголовок на странице примера, если ваш экран заведомо выше 200 пикселей?
rgb(255, 0, 0). Экран тут ни при чём: условие спросило окно того документа, в котором лежит стиль, а этот документ живёт в рамке высотой 140 пикселей. Страница учебника вокруг рамки о правиле ничего не знает, и у неё свои условия про своё окно.С шириной так не выйдет, и это стоит увидеть своими глазами. Рамка примера тянется на всю колонку текста: на широком мониторе в ней 732 пикселя, на телефоне шириной 320 — всего 270. Поэтому условие про ширину не даёт общего ответа: он верен только для той страницы, на которой пример запущен. Замените в примере max-height: 200px на min-width: 400px и запустите снова: на компьютере заголовок останется красным, а на телефоне, который держат вертикально, станет чёрным.
Условие не делает правила сильнее. Оно только включает их, а спор решает тот же судья каскада: при равной специфичности побеждает правило, записанное ниже. Если бы блок @media стоял над строкой .title { color: rgb(0, 0, 0); }, заголовок остался бы чёрным при любом окне. Поэтому условные правила пишут после основных.
@media спрашивает окно документа, в котором лежит стиль. У примера это рамка высотой 140 пикселей, у вашей страницы — вкладка.
Теперь задача, ради которой всё и затевается. Сетка шириной 240px, промежуток 10px, колонки 1fr 80px: широкая основная <main> и узкая боковая <aside>. В обе колонки положили одну и ту же карточку — картинку и полоску текста. Карточка собрана как flex-колонка, картинка над полоской, а правило @media ставит её в ряд, когда окно не уже 400 пикселей.
Сами направления карточек в статье не названы: они зависят от ширины вашей рамки. Прогноз: как лягут картинка и полоска на странице примера — одинаково в обеих карточках или по-разному?
Раньше это обходили арифметикой, и арифметика была хрупкой. Раз боковая колонка занимает, скажем, треть страницы, карточке в ней нужен свой порог, втрое больше, и отдельный класс вроде .card-in-sidebar со своим @media. То есть карточка обязана была знать, в какую колонку её положат. Её переносили в другое место или меняли ширину колонки — и пороги приходилось пересчитывать вручную, а забытый порог выглядел не как ошибка, а как «дизайнер так хотел».
@media знает ширину окна, но не ширину места: карточке в узкой колонке он отвечает то же, что карточке в широкой.
Контейнерный запрос спрашивает не окно, а коробку. Сначала предку объявляют, что он контейнер: container-type: inline-size. Слово inline здесь означает направление строки текста — в русском и английском это ширина. Потом условие пишут почти как в @media, только словом @container: @container (min-width: 120px) { … } читается как «ближайший контейнер не уже 120 пикселей». Проверяет его браузер для каждого элемента отдельно, и каждый спрашивает своего ближайшего предка-контейнера. Контейнерные запросы — Baseline widely с 14 августа 2025 года: пишите.
Страница та же, что в прошлом разделе. В стилях изменились две строки: колонки main и aside стали контейнерами, а @media (min-width: 400px) превратилось в @container (min-width: 120px). Прогноз — две строки: направления обеих карточек и x полоски текста в каждой. У картинки ширина 30px, промежуток в карточке 4px, а ширину колонки 1fr посчитайте, как в сетке: из ширины вычтите фиксированную колонку и промежуток.
Запрос переключает то же свойство flex-direction, которое ставит ряд и колонку во flex. Нового способа раскладки он не приносит, а только выбирает, какие правила действуют. Всё, что внутри, — обычный CSS.
Адаптивный компонент смотрит на своё место, а не на экран. Одно правило карточки работает в любой колонке, куда её положат.
Контейнером может быть любой предок, и запрос спрашивает ближайшего. Сам элемент в их число не входит: правило для .card не спросит ширину самой .card, даже если объявить контейнером её. Поэтому контейнером делают обёртку — колонку, клетку сетки, слот, — а стили меняют внутри; это вы посчитаете в первой задаче. Если контейнеры вложены друг в друга, а спросить нужно не ближайший, контейнеру дают имя, container-name: sidebar, и условие называет его: @container sidebar (min-width: 200px).
В свежем CSS условие часто записывают сравнением: (width >= 120px) значит то же, что (min-width: 120px), а (width < 120px) — «уже 120». Такая запись работает и в @container, и в @media. В @media она Baseline widely с 27 сентября 2025 года.
У объявления контейнера есть цена, и о ней стоит знать до того, как что-то сломается. Если бы ширина контейнера зависела от содержимого, а содержимое через запрос — от ширины контейнера, браузер ходил бы по кругу: картинка шире — контейнер вырос — условие сработало — картинка уже — контейнер сжался. Поэтому container-type: inline-size велит считать ширину элемента так, будто содержимого у него нет.
Обычному блоку в потоке это не мешает: его ширину и так даёт родитель. Колонке сетки тоже: её ширину задаёт дорожка. А теперь flex-ряд шириной 240px, в нём элемент без ширины, а внутри — картинка 100px. Ниже такой же ряд, только элемент объявлен контейнером. Прогноз — две строки: ширины обоих элементов, потом их высоты.
inline-size отключает содержимое только по ширине.Так же сжимается в ноль любой контейнер, который брал ширину у содержимого: элемент flex-ряда без width и без flex: 1, элемент с display: inline-block, плавающий или абсолютно позиционированный без ширины. Лечится шириной снаружи: width, flex: 1, клетка сетки или обычный блок в потоке. Бывает и container-type: size — контейнер по обеим осям. Но и высота у него тогда не берётся из содержимого: без явной height она нулевая, поэтому нужен он редко.
Контейнер сжался в ноль, а содержимое торчит наружу — проверьте, откуда он брал ширину. От содержимого нельзя, дайте её снаружи.
Условие переключает правила скачком: до 120 пикселей одно, с 120 — другое. Размер шрифта удобнее менять плавно, и для этого есть единицы контейнера. 1cqi — один процент ширины ближайшего контейнера: в контейнере 300px значение 8cqi даст 24px. Буквы расшифровываются как container query inline. Похожая единица vw — процент ширины окна, и у неё та же беда, что у @media: во всех колонках страницы она одинаковая.
Плавному размеру нужны края. В колонке 50px от 8cqi остаются нечитаемые 4px, а в очень широкой выходят огромные буквы. Функция clamp(наименьшее, желаемое, наибольшее) берёт желаемое значение и зажимает его между краями. clamp() — Baseline widely с 28 января 2023 года, единицы контейнера пришли вместе с контейнерными запросами.
Три контейнера шириной 100, 200 и 250px, у заголовка font-size: clamp(12px, 8cqi, 18px). Заполните таблицу на бумаге.
| Контейнер | 8cqi | После clamp() |
|---|---|---|
| 100px | ? | ? |
| 200px | ? | ? |
| 250px | ? | ? |
Прогноз — три размера шрифта через запятую, в порядке разметки. Считать их нужно от ширины своего контейнера, а не от окна.
clamp() поднял размер до 12px. 8% от 200 — 16px, между краями, и значение осталось как есть. 8% от 250 — 20px, больше верхнего края, и размер опустился до 18px. Контейнеры заданы в пикселях, поэтому ответ у всех читателей один.В настоящих стилях края обычно пишут в rem, например clamp(1rem, 8cqi, 1.5rem). Такой нижний край растёт вместе с размером шрифта, который человек выбрал в настройках браузера, а cqi об этой настройке ничего не знает. В примере края в пикселях: rem зависит от этой настройки, и ответ у разных читателей мог бы разойтись.
Желаемое — в cqi, края — в rem: шрифт следует за местом, но не становится мельче, чем просил человек.
Контейнерные запросы не отменили @media, а забрали у него компоненты. Вопросы про окно, устройство и человека по-прежнему задают через @media.
| Вопрос | Чем спросить |
|---|---|
| Хватает ли места карточке в её колонке | @container (min-width: …) |
| Помещается ли на экране боковая колонка | @media (min-width: …): это вопрос про окно |
| Сколько карточек влезет в ряд | без условий: repeat(auto-fill, minmax(…)) в сетке |
| Просил ли человек двигать поменьше | @media (prefers-reduced-motion: reduce) |
| Есть ли у устройства наведение мышью | @media (hover: hover) |
| Страницу печатают | @media print |
Порядок выбора получается такой. Сначала попробуйте обойтись без условий вовсе: сетка с auto-fill сама решает, сколько колонок влезет, а clamp() сам держит размер между краями. Компоненту, которому нужно перестроиться, дайте спросить свой контейнер. А окно спрашивают о раскладке всей страницы и о том, что знает только устройство: настройки человека, мышь или палец, экран или бумага. О настройке «меньше движения» подробно — в статье про @keyframes.
Условие про ширину окна проверяют на настоящей странице. Сохраните в файл страницу, в <style> которой стоит правило @media (max-width: 600px) { body { background: gold; } }, откройте её в браузере и сужайте окно: как только ширина станет 600 пикселей или меньше, фон пожелтеет. Если окно так узко не сжимается, включите в инструментах разработчика режим устройства, где ширину вписывают числом. В Chrome это кнопка Toggle device toolbar (Cmd+Shift+M в macOS, Ctrl+Shift+M в Windows), в Firefox — Responsive Design Mode (Cmd+Option+M и Ctrl+Shift+M).
На телефоне у того же файла есть ловушка из скелета страницы. Без строки <meta name="viewport" content="width=device-width, initial-scale=1"> мобильный браузер раскладывает страницу на ширину 980 пикселей, и для условия (max-width: 600px) окно телефона слишком широкое: фон не пожелтеет.
У @container есть и запросы по стилю: условие не про размер контейнера, а про значение его переменной CSS (переменные появятся дальше в главе). Такие запросы Baseline newly только с 19 мая 2026 года: знайте, что они есть, но пока не опирайтесь на них.
@mediaспрашивает окно документа, в котором лежит стиль. У примера учебника окно — рамка высотой 140 пикселей и шириной от 270 до 732, поэтому(max-height: 200px)срабатывает у всех, а ответ условия про ширину у каждого свой.- Окно у страницы одно: карточке в узкой колонке
@mediaотвечает то же, что карточке в широкой. Условные правила весят как обычные и пишутся после основных. container-type: inline-sizeобъявляет контейнер, а@container (min-width: …)спрашивает ближайшего предка-контейнера — сам элемент себя не спрашивает. Контейнерные запросы — Baseline widely с августа 2025 года.- Ширину контейнер не берёт у содержимого: во flex-ряду без ширины, с
inline-blockилиfloatон сжимается в ноль. Ширину ему дают снаружи —width,flex: 1, клеткой сетки. cqi— процент ширины контейнера,clamp(наименьшее, желаемое, наибольшее)зажимает значение между краями. Вместе они дают плавный размер от места, а края вremуважают настройки человека.@mediaостаётся для раскладки всей страницы и того, что знает только устройство: настроек человека, наведения, печати. Запросы по стилю к контейнеру — пока Baseline newly.
1/4В примере стоит @media (max-height: 200px). Вы открыли учебник на мониторе высотой 1080 пикселей. Сработает ли условие в рамке примера?
2/4Блок @media (max-height: 200px) { .title { color: red; } } записан выше строки .title { color: black; }. Какого цвета заголовок в рамке примера?
3/4Контейнер шириной 150px, у заголовка font-size: clamp(14px, 12cqi, 20px). Какой размер шрифта?
4/4Какой вопрос по-прежнему задают через @media, а не через @container?
Карточка не перестраивается
Контейнером объявили саму карточку и написали условие для неё же. Карточек две, шириной 100 и 200px. Сначала предскажите строку measure. Потом добейтесь, чтобы широкая карточка встала в ряд, а узкая осталась колонкой. Условие @container не трогайте, остальные стили и разметку — можно.
Вывод — .card → flex-direction: column, column. Запрос ищет ближайший контейнер среди предков карточки, а сама карточка в их число не входит. Предков-контейнеров у карточек нет, и условие не выполняется ни у одной, даже у широкой.
Контейнером делают обёртку и ширину переносят на неё:
Теперь прибор печатает .card → flex-direction: column, row: каждая карточка спрашивает свой слот, и в слоте 200px условие (min-width: 150px) выполнено.
Основная колонка пропала
Раскладка на flex: боковая колонка 80px и основная, объявленная контейнером, а в основной — карточка. В ряду 240px ей места хватает, но карточка стоит колонкой. Предскажите две строки measure. Потом допишите одно свойство, чтобы основная колонка заняла остаток ряда, а карточка встала в ряд.
Вывод — .layout > div → width: 80px, 0px и .card → flex-direction: column. У .main нет ни ширины, ни роста, и как flex-элемент она взяла бы ширину у содержимого. Но контейнер от содержимого отказался, и колонка сжалась в ноль. Условие (min-width: 120px) спрашивает контейнер нулевой ширины и не выполняется. На сцене ошибку не видно: карточка торчит из пустой колонки.
Ширину даём снаружи:
Теперь .layout > div → width: 80px, 160px и .card → flex-direction: row: flex: 1 отдаёт основной колонке всё свободное место, 240 − 80 = 160px, а это не меньше 120.
Заголовок от 12 до 22 пикселей
У заголовка размер 8cqi: в узком контейнере он мельчит, в широком разрастается. Предскажите три размера. Потом замените значение font-size одной записью clamp(), чтобы прибор напечатал 12px, 15px, 22px.
Вывод — .title → font-size: 8px, 12px, 20px: 8% от 100, 150 и 250.
Желаемое значение подбираем по среднему контейнеру: 15px от 150 — это 10%, то есть 10cqi. Края — 12px и 22px:
Прибор напечатает .title → font-size: 12px, 15px, 22px. 10px от 100 поднялись до нижнего края, 15px от 150 остались как есть, 25px от 250 опустились до верхнего.