Адаптивность без экрана: @container против @media

стили≈ 11 минут

Здесь будет встроен видео-разбор темы (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 пикселей?

js
разметка страницы
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера
разбор вывода
Красный, 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 пикселей.

Сами направления карточек в статье не названы: они зависят от ширины вашей рамки. Прогноз: как лягут картинка и полоска на странице примера — одинаково в обеих карточках или по-разному?

js
разметка страницы
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера
разбор вывода
Одинаково — у кого бы вы ни запустили. На широком мониторе обе карточки встали в ряд, на узком телефоне обе остались колонкой, но друг от друга они не отличаются никогда. Окно у страницы одно, и условие про него даёт один ответ всем, кто на ней лежит. В боковой колонке 80 пикселей карточке тесно при любом окне, а правило этого не видит.

Раньше это обходили арифметикой, и арифметика была хрупкой. Раз боковая колонка занимает, скажем, треть страницы, карточке в ней нужен свой порог, втрое больше, и отдельный класс вроде .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 посчитайте, как в сетке: из ширины вычтите фиксированную колонку и промежуток.

js
разметка страницы
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера
разбор вывода
Основная колонка шириной 240 − 80 − 10 = 150px, это не меньше 120, и карточка в ней встала в ряд: полоска справа от картинки, на 30 + 4 = 34px. Боковой колонке досталось 80px, условие не выполнено, и карточка осталась колонкой: полоска под картинкой, у левого края своей колонки, на 150 + 10 = 160px. От ширины вашей рамки ответ не зависит: сетка и её колонки заданы в пикселях.

Запрос переключает то же свойство 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. Ниже такой же ряд, только элемент объявлен контейнером. Прогноз — две строки: ширины обоих элементов, потом их высоты.

js
разметка страницы
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера
разбор вывода
Первый элемент взял ширину у содержимого, 100px, как и положено flex-элементу без ширины. Контейнер сжался в ноль: содержимого для ширины у него больше нет. Картинка при этом никуда не делась, она торчит из пустого контейнера, и на сцене два ряда выглядят одинаково. Высоты у обоих 20px: 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??

Прогноз — три размера шрифта через запятую, в порядке разметки. Считать их нужно от ширины своего контейнера, а не от окна.

js
разметка страницы
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера
разбор вывода
8% от 100 — это 8px, меньше нижнего края, и 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 не трогайте, остальные стили и разметку — можно.

js
разметка страницы
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Вывод — .card → flex-direction: column, column. Запрос ищет ближайший контейнер среди предков карточки, а сама карточка в их число не входит. Предков-контейнеров у карточек нет, и условие не выполняется ни у одной, даже у широкой.

Контейнером делают обёртку и ширину переносят на неё:

jsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Теперь прибор печатает .card → flex-direction: column, row: каждая карточка спрашивает свой слот, и в слоте 200px условие (min-width: 150px) выполнено.

Основная колонка пропала

Раскладка на flex: боковая колонка 80px и основная, объявленная контейнером, а в основной — карточка. В ряду 240px ей места хватает, но карточка стоит колонкой. Предскажите две строки measure. Потом допишите одно свойство, чтобы основная колонка заняла остаток ряда, а карточка встала в ряд.

js
разметка страницы
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Вывод — .layout > div → width: 80px, 0px и .card → flex-direction: column. У .main нет ни ширины, ни роста, и как flex-элемент она взяла бы ширину у содержимого. Но контейнер от содержимого отказался, и колонка сжалась в ноль. Условие (min-width: 120px) спрашивает контейнер нулевой ширины и не выполняется. На сцене ошибку не видно: карточка торчит из пустой колонки.

Ширину даём снаружи:

jsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Теперь .layout > div → width: 80px, 160px и .card → flex-direction: row: flex: 1 отдаёт основной колонке всё свободное место, 240 − 80 = 160px, а это не меньше 120.

Заголовок от 12 до 22 пикселей

У заголовка размер 8cqi: в узком контейнере он мельчит, в широком разрастается. Предскажите три размера. Потом замените значение font-size одной записью clamp(), чтобы прибор напечатал 12px, 15px, 22px.

js
разметка страницы
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Вывод — .title → font-size: 8px, 12px, 20px: 8% от 100, 150 и 250.

Желаемое значение подбираем по среднему контейнеру: 15px от 150 — это 10%, то есть 10cqi. Края — 12px и 22px:

jsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Прибор напечатает .title → font-size: 12px, 15px, 22px. 10px от 100 поднялись до нижнего края, 15px от 150 остались как есть, 25px от 250 опустились до верхнего.