Container queries и :has(): CSS, который заменил половину скриптов

Запросы к контейнеру и родительский селектор поддерживаются везде. Показываем, какие задачи теперь решаются без JavaScript.

CФронтенд

Две возможности 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, потом улучшаем её там, где есть место.

4 мин чтения
ЕГЕРПРО

ЕГЕРПРО

Команда студии ЕГЕРПРО: разработчики, дизайнеры, SEO-специалисты и специалисты по технологиям искусственного интеллекта. Создаём сайты и веб-сервисы, внедряем решения на основе искусственного интеллекта и автоматизацию, работаем над скоростью и продвижением сайтов. Пишем только о технологиях и подходах, которые проверили на реальных клиентских проектах.

Нужен сайт, который так же разобран по деталям?

Оценим задачу бесплатно и пришлём смету со сроками.

Читайте также

Похожие статьи

Сайт в разработке информация наполняется. Часть разделов может быть неполной.