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

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

Адрес: https://egerpro.ru/blog/container-queries-i-has/
Обновлено: 2026-08-11

Две возможности 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](https://egerpro.ru/blog/inp-vmesto-fid-podgotovka/) на странице записи улучшился на 35 мс.

## Как начать использовать

Обе возможности деградируют мягко: в старом браузере правило просто не применится. Это значит, что их можно добавлять в существующий проект как улучшение, не заботясь о запасных вариантах — при условии, что базовая раскладка работает и без них.

> ## Почему медиазапросов было недостаточно
>
>
>
> Медиазапрос знает только ширину окна. Компонент же живёт в контейнере, и одна и та же карточка может стоять в широкой колонке и в узкой боковой панели при одинаковом окне. Раньше это решалось классами-модификаторами, которые приходилось помнить и передавать через все уровни разметки.
>
>
>
> Контейнерные запросы убирают эту связку: компонент смотрит на своё окружение, а не на окно. Практически это значит, что карточку можно перенести в любое место сетки, и она перестроится сама — без нового класса и без правки шаблона.
>
>
>
> Селектор :has() закрывает вторую половину той же проблемы. Раньше стиль родителя, зависящий от содержимого, требовал скрипта: проверить, есть ли внутри картинка, и навесить класс. Теперь это одна строка CSS, которая работает до загрузки JavaScript и не мигает при первой отрисовке.
>
>
>
> ## Чем за это платишь
>
>
>
> Контейнерные запросы требуют объявить контейнер, а объявление создаёт новый контекст: элемент внутри перестаёт быть позиционным родителем прежним образом. На готовой вёрстке это иногда ломает абсолютное позиционирование, и разбираться приходится вручную.
>
>
>
> У :has() своя цена — производительность при неаккуратном использовании. Селектор вида «любой элемент, внутри которого есть любой элемент с классом» заставляет браузер проверять много узлов. На больших страницах это заметно. Мы держим такие правила узкими и привязываем к конкретному предку.
>
>
>
> ## Частые вопросы
>
>
>
> **Что делать со старыми браузерами?**
>
>
>
> Обе возможности поддерживаются во всех актуальных браузерах несколько лет. Для остальных пишем базовый вариант вёрстки и добавляем улучшения через @supports: пользователь старого браузера видит рабочую страницу, просто без части адаптации.
>
>
>
> **Можно ли полностью отказаться от медиазапросов?**
>
>
>
> Нет, и не нужно. Медиазапросы остаются для того, для чего и были: размеры страницы целиком, печать, тёмная тема, уменьшенная анимация. Контейнерные запросы — для компонентов. Это разные уровни, а не замена.
>
>
>
> Хорошее правило: сначала пишем раскладку, которая работает без container queries, потом улучшаем её там, где есть место.
