Две возможности CSS, которых ждали годами, наконец поддерживаются всеми браузерами. Вместе они убирают заметный кусок кода, который раньше приходилось писать на JavaScript.
Container queries: компонент знает свой размер
Медиазапросы смотрят на ширину экрана. Проблема в том, что компоненту важна не ширина экрана, а ширина места, в котором он оказался. Одна и та же карточка товара может стоять в узкой колонке сайдбара и в широкой сетке каталога.
Раньше это решали модификаторами: card--compact, card--wide. Разработчик вручную проставлял класс в зависимости от места. Теперь карточка определяет это сама.
.card-grid {
container-type: inline-size;
}
.card {
display: grid;
gap: 1rem;
}
/* Широкое место — раскладываем в две колонки */
@container (min-width: 480px) {
.card {
grid-template-columns: 200px 1fr;
}
}
Компонент становится по-настоящему переиспользуемым: его можно положить куда угодно, и он подстроится. На проекте с каталогом это убрало около двухсот строк модификаторов.
:has() — родительский селектор
Селектор :has() позволяет стилизовать элемент в зависимости от того, что внутри него. Раньше это было возможно только через JavaScript.
Карточка с изображением и без
/* Отступ сверху только если картинки нет */
.card:not(:has(img)) {
padding-top: 2rem;
}
Подсветка поля с ошибкой
/* Обёртка краснеет, когда поле внутри невалидно */
.field:has(input:user-invalid) {
border-color: var(--destructive);
}
.field:has(input:focus) {
border-color: var(--foreground);
}
Второй пример особенно показателен: подсветка обёртки при фокусе на поле — типовая задача, ради которой раньше вешали обработчики focus и blur на каждое поле формы.
Блокировка прокрутки при открытом меню
/* Без единой строки JavaScript */
body:has(#menu-toggle:checked) {
overflow: hidden;
}
Про производительность
Селектор :has() считается дорогим, но на практике это заметно только при обходе больших поддеревьев. Ограничивайте область: .form:has(...) вместо body:has(...) там, где это возможно.
Что мы убрали с реального проекта
На сайте клиники после перехода на эти возможности:
- Скрипт расстановки модификаторов карточек — минус 3,4 КБ
- Обработчики фокуса на полях формы — минус 1,8 КБ
- Логика блокировки скролла — минус 0,6 КБ
- Итого около 6 КБ JavaScript и три источника потенциальных багов
Шесть килобайт сами по себе немного. Важнее, что этот код больше не выполняется в главном потоке при каждом взаимодействии — INP на странице записи улучшился на 35 мс.
Как начать использовать
Обе возможности деградируют мягко: в старом браузере правило просто не применится. Это значит, что их можно добавлять в существующий проект как улучшение, не заботясь о запасных вариантах — при условии, что базовая раскладка работает и без них.
Почему медиазапросов было недостаточно
Медиазапрос знает только ширину окна. Компонент же живёт в контейнере, и одна и та же карточка может стоять в широкой колонке и в узкой боковой панели при одинаковом окне. Раньше это решалось классами-модификаторами, которые приходилось помнить и передавать через все уровни разметки.
Контейнерные запросы убирают эту связку: компонент смотрит на своё окружение, а не на окно. Практически это значит, что карточку можно перенести в любое место сетки, и она перестроится сама — без нового класса и без правки шаблона.
Селектор :has() закрывает вторую половину той же проблемы. Раньше стиль родителя, зависящий от содержимого, требовал скрипта: проверить, есть ли внутри картинка, и навесить класс. Теперь это одна строка CSS, которая работает до загрузки JavaScript и не мигает при первой отрисовке.
Чем за это платишь
Контейнерные запросы требуют объявить контейнер, а объявление создаёт новый контекст: элемент внутри перестаёт быть позиционным родителем прежним образом. На готовой вёрстке это иногда ломает абсолютное позиционирование, и разбираться приходится вручную.
У :has() своя цена — производительность при неаккуратном использовании. Селектор вида «любой элемент, внутри которого есть любой элемент с классом» заставляет браузер проверять много узлов. На больших страницах это заметно. Мы держим такие правила узкими и привязываем к конкретному предку.
Частые вопросы
Что делать со старыми браузерами?
Обе возможности поддерживаются во всех актуальных браузерах несколько лет. Для остальных пишем базовый вариант вёрстки и добавляем улучшения через @supports: пользователь старого браузера видит рабочую страницу, просто без части адаптации.
Можно ли полностью отказаться от медиазапросов?
Нет, и не нужно. Медиазапросы остаются для того, для чего и были: размеры страницы целиком, печать, тёмная тема, уменьшенная анимация. Контейнерные запросы — для компонентов. Это разные уровни, а не замена.
Хорошее правило: сначала пишем раскладку, которая работает без container queries, потом улучшаем её там, где есть место.