Вложенность, @layer и контейнерные запросы

Урок про три современных механизма организации стилей: нативную вложенность CSS, каскадные слои @layer и контейнерные запросы для компонентной адаптивности.

Нативная вложенность позволяет писать вложенные правила без препроцессора, @layer явно управляет приоритетом каскада, а container queries адаптируют компонент по размеру его контейнера, а не всего окна.

Зачем это на практике

Три вещи, ради которых раньше подключали Sass и придумывали методологии, теперь умеет сам браузер. Вложенность убирает повторение длинных префиксов. @layer решает вечную войну специфичности и порядка подключения. А контейнерные запросы наконец делают компоненты по-настоящему переиспользуемыми: карточка сама знает, как выглядеть в узкой колонке и в широкой, не привязываясь к ширине экрана.

Нативная вложенность CSS

Теперь правила можно вкладывать прямо в CSS. Вложенный селектор раскрывается относительно родительского. Символ & ссылается на сам родительский селектор.

.card {
  padding: 16px;
  background: var(--card, #f3f4f6);

  /* = .card .title */
  .title { font-weight: 600; }

  /* & — это .card; = .card:hover */
  &:hover { box-shadow: 0 4px 12px rgba(0,0,0,0.1); }

  /* = .card > header .title (вложенность глубже) */
  & > header .title { color: #2563eb; }

  /* медиа-запрос тоже можно вкладывать */
  @media (min-width: 600px) {
    padding: 24px;
  }
}

Важная тонкость: без & вложенный селектор означает потомка. Чтобы получить «тот же элемент с модификатором», & обязателен — &.active даёт .card.active, а просто .active внутри даст .card .active (другой элемент). Это главное отличие от привычного поведения Sass, где .active в некоторых стилях по умолчанию тоже трактовался как потомок, но &-логика здесь строже.

@layer — управление каскадом

Каскадные слои дают приоритет между группами стилей, который сильнее обычной специфичности. Порядок объявления слоёв определяет их вес: чем позже объявлен слой, тем он «главнее».

/* Объявляем порядок один раз: reset слабее всех, utilities — сильнее */
@layer reset, base, components, utilities;

@layer base {
  /* даже #id здесь проиграет слою выше */
  a { color: #2563eb; }
}

@layer components {
  .link { color: rebeccapurple; }
}

@layer utilities {
  .text-muted { color: #6b7280; }
}

Революция в том, что слой бьёт специфичность: правило из utilities победит правило из base, даже если в base селектор гораздо специфичнее. Это решает классическую боль — когда служебный класс .text-muted не мог перебить какой-нибудь .article #content a. Стили вне любых слоёв считаются «важнее» всех именованных слоёв (кроме случаев с !important, где порядок переворачивается).

Слои особенно полезны при подключении чужого CSS: оборачиваете библиотеку в @layer vendor, и ваши собственные стили в более позднем слое спокойно её перекрывают без !important.

Контейнерные запросы (container queries)

Медиа-запросы смотрят на окно. Но переиспользуемому компоненту важна не ширина экрана, а ширина места, куда его положили. Контейнерные запросы отвечают именно на это.

Сначала родителя объявляют контейнером через container-type, затем дочерние стили реагируют на его размер через @container.

/* 1. Назначаем контекст: следим за шириной (inline-осью) */
.card-host {
  container-type: inline-size;
  container-name: card;
}

/* 2. Стили карточки зависят от ширины контейнера, не окна */
.card { display: block; }

@container card (min-width: 400px) {
  .card {
    display: grid;
    grid-template-columns: 120px 1fr;
    gap: 16px;
  }
}

Одна и та же карточка в узком сайдбаре останется вертикальной, а в широкой основной колонке станет двухколоночной — потому что условие проверяет её собственный контейнер. Компонент перестаёт зависеть от того, на какой странице он живёт. Появились и контейнерные единицы: cqw (1% ширины контейнера), cqi, cqb — например, font-size: 5cqi масштабирует текст относительно контейнера, а не окна.

Как это работает под капотом

Вложенность — это синтаксический сахар: браузер раскрывает вложенные правила в плоские эквиваленты на этапе парсинга, специфичность считается по итоговому селектору. @layer добавляет в каскад ещё один уровень сравнения перед специфичностью: сначала браузер смотрит на важность и слой, и только при равенстве слоёв переходит к специфичности и порядку. Container queries требуют от движка containment: объявляя container-type: inline-size, вы обещаете браузеру, что внутренний контент не влияет на ширину контейнера «обратно», — это разрывает потенциальный цикл «размер зависит от стилей, которые зависят от размера» и делает запросы вычислимыми.

Частые ошибки

  • Забыть & для модификатора. Внутри .card правило .active означает потомка .card .active; нужный .card.active даёт только &.active.
  • Думать, что @layer — про специфичность внутри слоя. Внутри одного слоя всё решает обычная специфичность; слои сравниваются только между собой.
  • Перепутать порядок слоёв. Главнее тот, что объявлен позже. Часто список @layer забывают объявить заранее, и порядок задаётся случайно — первым появлением.
  • Запрашивать контейнер, не объявив его. Без container-type на предке @container не сработает.
  • Ставить container-type на сам адаптируемый элемент. Контейнером должен быть родитель — элемент не может запрашивать собственный размер, который сам же меняет.

Итоги

  • Нативная вложенность убирает повтор префиксов; & — ссылка на родительский селектор, без неё вложенное правило означает потомка.
  • @layer вводит приоритет между группами стилей, который сильнее специфичности; главнее слой, объявленный позже.
  • Слои идеальны для укрощения чужих библиотек без !important.
  • Container queries адаптируют компонент по ширине его контейнера (container-type + @container), а не по ширине окна.
  • Появились контейнерные единицы cqi/cqw/cqb для размеров относительно контейнера.
Проверьте себя
1. Что означает селектор .active внутри вложенного правила .card { .active { ... } } (без амперсанда)?
AТот же элемент с классом active, то есть .card.active
BПотомка .card с классом active, то есть .card .active
CЛюбой элемент .active на странице
DЭто синтаксическая ошибка
2. Правило в слое utilities и правило в слое base конфликтуют, причём в base селектор гораздо специфичнее. Кто победит, если порядок объявлен как @layer base, utilities;?
AПобедит base — у него выше специфичность
BПобедит utilities — слой, объявленный позже, главнее, и это сильнее специфичности
CПобедит тот, что ниже в файле
DСработает !important автоматически
3. Зачем нужны контейнерные запросы (@container), если уже есть медиа-запросы?
AОни быстрее работают
BОни позволяют компоненту адаптироваться по ширине его контейнера, а не всего окна — поэтому один компонент корректно выглядит и в узкой, и в широкой колонке
CОни заменяют JavaScript полностью
DЭто просто новый синтаксис для тех же медиа-запросов