Как писать CSS с @scope: Полный разбор синтаксиса, изоляция стилей и donut scoping

В декабре 2025 года в CSS произошло событие, которое сложно переоценить: последний из основных браузеров — Firefox 146 — добавил поддержку правила @scope. Это означает, что технология получила статус Baseline Newly Available и готова к промышленному использованию во всех современных браузерах без исключений.

Введение

В предыдущем материале — «Полное руководство по CSS @scope: Изоляция стилей без BEM и Tailwind» — подробно разобрали, почему @scope стал необходим индустрии: как он решает проблемы глобальности CSS, упрощает BEM-структуры и меняет правила игры со специфичностью.

Задача этой статьи — другая.

Не будем повторно сравнивать @scope с методологиями или обсуждать философию изоляции стилей. Вместо этого сфокусируемся на сугубо практических вопросах:

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

Перейдём к анатомии @scope и разберём его синтаксис по косточкам.

Анатомия @scope: Разбираем синтаксис по частям

Прежде чем погружаться в сценарии использования, нужно чётко понимать, из каких элементов состоит правило @scope и как эти элементы взаимодействуют. Синтаксис лаконичен, но за его краткостью скрываются нюансы.

Корень скоупа (<scope-root>)

Первый и обязательный элемент — это корень скоупа. Вы указываете селектор, и стили внутри блока @scope будут применяться только к элементам, находящимся внутри этого корня (включая его самого).

Базовый пример:

@scope (.article-card) {
/* Стили применяются только к элементам внутри .article-card */
h2 {
font-size: 1.5rem;
margin-bottom: 0.5rem;
}

p {
line-height: 1.6;
color: #333;
}
}

В этом фрагменте заголовки h2 и параграфы p, размещённые внутри элемента с классом article-card, получат указанные стили. Те же теги за пределами карточки останутся без изменений.

Важное отличие от вложенности: На первый взгляд кажется, что @scope (.card) { h2 { ... } } делает то же, что и вложенный селектор .card h2 { ... }. Технически — да, стили применяются к одним и тем же элементам. Но поведение при переопределении будет иным из-за механизма близости (proximity), о котором подробно поговорим в разделе про специфичность.

В качестве селектора корня можно использовать не только классы, но и другие селекторы: теги, идентификаторы, атрибуты.

/* Селектор по тегу */
@scope (article) {
/* ... */
}

/* Селектор по атрибуту */
@scope ([data-component="modal"]) {
/* ... */
}

Предел скоупа (<scope-root> to <scope-limit>)

Самая интересная возможность @scope, у которой нет прямых аналогов в нативном CSS, — это установка нижней границы действия стилей. Такой синтаксис называют «донат-скоупингом» (donut scoping), поскольку область действия напоминает бублик — есть внешний радиус (корень) и внутренний (предел).

@scope (.navigation) to (.dropdown) {
/* Стили применяются внутри .navigation, но НЕ внутри .dropdown */
a {
color: blue;
font-weight: 500;
}
}

Что происходит в этом примере:

  1. Корень скоупа: .navigation — все ссылки внутри навигации потенциально могут попасть под действие правил.
  2. Предел скоупа: .dropdown — как только браузер встречает элемент с классом dropdown внутри навигации, действие стилей для этого элемента и всего его содержимого прекращается.

Представьте типичную навигацию с выпадающими меню. Ссылки в основной панели должны быть полужирными и синими, а ссылки внутри выпадающих списков — обычными и, скажем, чёрными. Без @scope пришлось бы писать сбросы:

.navigation a {
color: blue;
font-weight: 500;
}

.navigation .dropdown a {
color: black; /* сброс */
font-weight: normal; /* сброс */
}

Со @scope намерение выражается напрямую: «стилизуй ссылки в навигации, но не трогай те, что внутри дропдаунов». Код становится декларативным, а количество селекторов не растёт с каждым новым исключением.

Предел скоупа не включается в область действия. Это значит, что сам элемент .dropdown и всё его поддерево не получают стилей из данного блока @scope, даже если они подходят под селекторы.

Специальные селекторы внутри @scope: :scope и &

Внутри блока @scope у вас появляются два служебных селектора, которые ссылаются на корень скоупа.

:scope — псевдокласс, который в контексте @scope всегда указывает на корневой элемент.

@scope (.user-card) {
/* Стили для самого корневого элемента */
:scope {
border: 1px solid #ddd;
border-radius: 8px;
padding: 16px;
}

/* Стили для прямых потомков корня */
:scope > * {
margin-bottom: 8px;
}
}

Использовать :scope необязательно — тот же эффект даст простой селектор .user-card. Но явное указание на корень повышает читаемость кода, особенно в длинных скоупах, где сложно уследить за границами.

& — вложенный селектор из спецификации CSS Nesting, который в контексте @scope работает аналогично :scope. Это вопрос скорее стиля кодирования: некоторые разработчики предпочитают более краткую запись.

@scope (.user-card) {
/* То же самое, что и :scope */
& {
border: 1px solid #ddd;
}

/* Прямые потомки */
& > * {
margin-bottom: 8px;
}
}

На этом разбор базовой анатомии можно считать завершённым. В следующем разделе перейдём к самому важному с практической точки зрения — где именно писать @scope и какие последствия имеет каждый из способов подключения.

Три лица @scope: Где и как писать код

Одна из сильных сторон @scope — его универсальность. Это правило можно использовать в любом месте, где допустим CSS: во внешних таблицах, во внутренних <style>-блоках и даже в атрибуте style (хотя последний сценарий экзотичен и редко нужен на практике). Но у каждого способа подключения — свои характеристики производительности, удобства поддержки и повторного использования.

В этом разделе разберём три основных лица @scope и критерии выбора между ними.

В CSS-файлах (Классический подход)

Это самый очевидный и привычный способ. Вы подключаете внешний файл стилей через <link> и внутри него используете @scope так же, как любые другие at-правила.

/* components/cards.css */
@scope (.product-card) {
:scope {
display: flex;
flex-direction: column;
}

h3 {
font-size: 1.25rem;
margin: 0.5rem 0;
}

.price {
font-weight: bold;
color: var(--price-color);
}
}

Когда выбирать этот способ:

Плюсы:

Минусы:

Этот способ идеально подходит для базовых стилей, библиотек компонентов и всего, что не меняется от страницы к странице.

В <style> внутри HTML (Изоляция компонентов)

Второй способ — размещать @scope непосредственно в <style>-блоке внутри HTML-документа. Здесь появляется важная особенность: если не указывать корневой селектор, скоупом становится родительский элемент для <style>.

<section class="promo-block">
<style>
@scope {
/* Корень скоупа - <section class="promo-block"> */
:scope {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 3rem;
}

h2 {
font-size: 2rem;
margin-bottom: 1rem;
}

p {
opacity: 0.9;
}
}
</style>

<h2>Специальное предложение</h2>
<p>Только сегодня скидка 30% на все курсы</p>
</section>

В этом примере корнем скоупа автоматически стал элемент <section>, внутри которого находится <style>. Стили применяются только к содержимому этой секции и не влияют на остальную страницу.

Когда выбирать этот способ:

Плюсы:

Минусы:

Этот способ отлично работает для одноразовых акций, посадочных страниц и ситуаций, где критична каждая миллисекунда загрузки.

Комбинированный подход

В реальных проектах редко можно обойтись чем-то одним. Наиболее прагматичная стратегия — комбинировать оба подхода, используя сильные стороны каждого.

Типичная архитектура:

<!DOCTYPE html>
<html>
<head>
<!-- 1. Глобальные стили для всего сайта -->
<link rel="stylesheet" href="global.css">

<!-- 2. Стили раздела, подключаются только на главной -->

</head>
<body>
<header>...</header>

<main>
<!-- 3. Уникальный промо-блок со встроенными стилями -->
<section class="special-offer">
<style>
@scope {
/* стили только для этого блока */
}
</style>
<h2>...</h2>
</section>

<!-- Стандартные компоненты, стилизованные через внешний CSS -->
<div class="product-grid">...</div>
</main>
</body>
</html>

Критерии выбора в одной таблице:

МетодКогда использоватьПроизводительностьПовторное использование
Внешний CSS-файлКомпонент на 80%+ страниц, базовые стили, библиотекиРендер-блокирующий, но кэшируетсяВысокое (DRY)
Внешний CSS по условиюСтили крупного раздела (каталог, личный кабинет)Блокирует рендер только при загрузкеСреднее (один раз на раздел)
<style> со @scopeУникальный компонент на одной страницеНе блокирует рендер, нет кэшаНизкое (только здесь)

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

:scope за пределами @scope

Псевдокласс :scope появился в CSS задолго до @scope и имеет собственную, независимую жизнь. Понимание этой «второй сущности» важно, чтобы не путаться при чтении чужого кода и эффективно использовать :scope в JavaScript.

Глобальный :scope: когда скоуп — весь документ

Вне блока @scope псевдокласс :scope ведёт себя предсказуемо: он ссылается на корневой элемент скоупа. Но какой скоуп может быть у глобального CSS? Правильно — весь документ.

Таким образом, на уровне корня документа :scope — это то же самое, что и :root, и просто селектор html.

/* Три способа сказать одно и то же */
html {
background: #f5f5f5;
}

:root {
background: #f5f5f5;
}

:scope {
background: #f5f5f5;
}

Зачем нужны три дублирующих друг друга селектора? Исторически сложилось:

На практике в глобальных стилях нет причин использовать :scope вместо :root или html. Но знать об этой возможности полезно — вы можете встретить такой код в проектах, где разработчики стремятся к единообразию или автоматически транспилируют стили.

:scope в JavaScript: выборка прямых детей без сюрпризов

Гораздо важнее и полезнее роль :scope в DOM-методах: querySelector(), querySelectorAll(), matches() и closest(). Здесь :scope позволяет решить давнюю проблему — выборку только непосредственных потомков элемента.

Рассмотрим типичную ситуацию. Есть разметка:

<section id="comments">
<div class="comment">Первый комментарий</div>
<div class="comment">Второй комментарий</div>
<div class="comment">
Третий комментарий с ответом
<div class="comment">Ответ на третий</div>
</div>
</section>

Необходимо выбрать только «корневые» комментарии первого уровня, исключая вложенные ответы. Интуиция подсказывает использовать селектор > .comment, но в JavaScript он не сработает напрямую:

const commentsSection = document.getElementById('comments');

// Так НЕ работает - вернёт пустую коллекцию
commentsSection.querySelectorAll('> .comment');

// А так работает - благодаря :scope
const topLevelComments = commentsSection.querySelectorAll(':scope > .comment');
console.log(topLevelComments.length); // 3 (только комментарии первого уровня)

Почему так? Метод querySelectorAll выполняет выборку в контексте элемента, на котором вызван. Но селектор > .comment сам по себе не знает, относительно какого элемента считать «родителем». Добавление :scope явно указывает: «родитель — это элемент, на котором вызван метод».

Другие примеры использования:

const element = document.querySelector('.some-class');

// Проверка, является ли элемент прямым потомком своего родителя
if (element.matches(':scope > *')) {
console.log('Элемент - прямой потомок своего родителя');
}

// Поиск ближайшего родителя с классом 'container', но не поднимаясь выше определённой границы
// (это уже сложнее, но :scope может участвовать в таких сценариях)

Интересно, что внутри @scope в CSS селектор :scope > * работает точно так же, как и в JavaScript — выбирает прямых потомков корня скоупа. Это не случайное совпадение, а продуманная симметрия между CSS и DOM API.

На этом разбор синтаксиса и мест применения @scope можно считать завершённым. В заключительном разделе соберём все части воедино и предложим дорожную карту для внедрения @scope в проект — от первого эксперимента до полноценного использования в продакшене.

Часто задаваемые вопросы

Можно ли использовать @scope прямо сейчас в продакшене?

Да, можно. С января 2026 года @scope поддерживается во всех основных браузерах: Chrome 120+, Firefox 146+, Safari 17.4+. Статус Baseline Newly Available означает, что технология стабильна и рекомендована к использованию. Если вам нужна поддержка очень старых браузеров (например, Internet Explorer или ранних версий мобильных браузеров), предусмотрите фолбэк в виде обычных вложенных селекторов или постепенную деградацию.

Чем @scope отличается от простой вложенности (CSS Nesting)?

Это принципиально разные механизмы. Вложенность — это синтаксический сахар для составных селекторов:

.card {
& h2 { } /* Превращается в .card h2 */
}

@scope — это создание новой контекстной области с особыми правилами разрешения конфликтов (близость, а не порядок объявления). Кроме того, @scope позволяет установить нижнюю границу (to), чего вложенность делать не умеет.

Что произойдёт, если внутри одного @scope использовать ещё один @scope?

Вложенные скоупы работают предсказуемо. Внутренний @scope создаёт свою область действия, ограниченную своим корнем и, если указано, пределом. При этом механизм близости применяется на каждом уровне: при конфликте стилей побеждает тот скоуп, чей корень находится ближе к целевому элементу в иерархии DOM.

@scope (.outer) {
span { color: red; }

@scope (.inner) {
span { color: blue; }
}
}

В этом примере спаны внутри .inner будут синими (ближайший скоуп), а спаны внутри .outer, но вне .inner, — красными.

Как @scope влияет на специфичность?

Сами по себе правила внутри @scope имеют ту же специфичность, что и аналогичные селекторы вне скоупа. Но при конфликте двух правил с одинаковой специфичностью побеждает не то, что объявлено позже в файле (как в обычном CSS), а то, чей корень скоупа находится ближе к целевому элементу. Это правило близости (proximity) заменяет порядок каскада в контексте скоупов.

Подробно этот механизм разобран в статье «Управление специфичностью: замена !important и искусственных селекторов» из предыдущего руководства.

Нужно ли теперь отказываться от БЭМ?

Не обязательно. @scope даёт альтернативный способ изоляции, но БЭМ решает и другие задачи — даёт семантику в разметке, упрощает навигацию по коду, формирует общий язык в команде. Разумный подход — комбинировать: использовать БЭМ для именования корневых элементов, а внутри скоупа писать более простые селекторы по тегам.

Кратко это рассматривается в «Упрощение BEM-структуры: класс только на родителе». Более развёрнутый ответ на этот вопрос есть в статье «Как CSS @scope может заменить БЭМ».

Поддерживает ли @scope медиазапросы?

Да, @scope можно комбинировать с любыми другими at-правилами. Медиазапросы могут находиться как внутри @scope, так и снаружи, оборачивая скоупы.

@media (max-width: 768px) {
@scope (.sidebar) {
/* стили для мобильной версии сайдбара */
}
}
Почему селектор :scope > * не работает так, как я ожидаю?

Самая частая причина — неправильное понимание границ скоупа. Проверьте:

  1. Действительно ли элемент, который вы хотите выбрать, находится внутри корня скоупа?
  2. Не попадает ли он в зону действия предела (to ...), если вы его использовали?
  3. Нет ли другого, более близкого скоупа, который переопределяет стили?

Если проблема не в этом, проверьте, не перебивается ли ваше правило другим с той же специфичностью, но из более близкого скоупа.

Влияет ли @scope на производительность?

Влияние на производительность рендеринга минимально и в подавляющем большинстве случаев неотличимо от обычных селекторов. Браузеры оптимизируют обработку скоупов так же, как и любых других правил. Гораздо большее влияние на производительность оказывает способ подключения CSS (внешний файл против встроенного), что мы подробно разбирали в разделе про три лица @scope.

Можно ли использовать @scope с препроцессорами (Sass, Less)?

Да, можно. Все современные препроцессоры корректно обрабатывают @scope как нативное CSS-правило и не изменяют его. Однако внутри @scope вы можете использовать возможности препроцессора — переменные, миксины, вложенность — точно так же, как и в любом другом месте.

Где посмотреть реальные примеры кода?

В этой статье все примеры самодостаточны и готовы к копированию. Кроме того, в разделе «Типичный сценарий: день из жизни поддержки CSS» из первого руководства показана эволюция реального кода от проблемной реализации к чистому решению с @scope.

Заключение и дорожная карта

Мы проделали большой путь: от базового синтаксиса @scope до нюансов его применения в разных контекстах и даже заглянули в поведение :scope в JavaScript. Пришло время собрать все части воедино и ответить на главный практический вопрос: с чего начать внедрение @scope в проект уже завтра утром?

Что мы теперь знаем

  1. Синтаксис @scope состоит из корня (обязательно) и предела (опционально). Предел создаёт «донат-скоупинг» — область действия в виде кольца, что позволяет элегантно обходить вложенные компоненты.
  2. Внутри скоупа можно использовать :scope и `& для явного указания на корневой элемент — это повышает читаемость и даёт единообразие.
  3. Контекст применения радикально меняет характеристики кода:
    • Внешние CSS-файлы — переиспользуемость и кэширование.
    • Встроенные <style>-блоки — скорость загрузки и изоляция.
    • Комбинированный подход — прагматичный баланс для реальных проектов.
  4. :scope живёт и вне @scope — в DOM-методах он незаменим для выборки прямых потомков, а в глобальных стилях дублирует :root (что полезно знать, но редко нужно использовать).

Дорожная карта внедрения

@scope не требует мгновенного рефакторинга всей кодовой базы. Это инструмент для постепенного улучшения. Вот реалистичный план из трёх этапов:

Этап 1. Разведка (одна-две итерации)

Выберите изолированный компонент, который сейчас:

Перепишите его стили с @scope. Убедитесь, что поведение предсказуемо во всех целевых браузерах. Сравните объём кода и читаемость с исходным вариантом.

Этап 2. Локальный стандарт (следующие несколько недель)

Если эксперимент удался, зафиксируйте решение в команде:

Начните использовать @scope во всех новых компонентах, которые естественно попадают под его применение

Этап 3. Стратегический рефакторинг (по мере необходимости)

Постепенно, при плановых доработках существующих компонентов, заменяйте старые конструкции с каскадными войнами и !important на чистые @scope-блоки. Это не должно быть отдельной задачей — просто улучшайте код при каждом касании.

Вместо послесловия

@scope — не серебряная пуля и не замена методологиям вроде BEM или инструментам вроде Tailwind. Это новый примитив языка, который закрывает брешь, существовавшую двадцать лет. Теперь появился нативный способ сказать браузеру: «вот здесь начинается компонент, а здесь заканчивается, и за эти границы стили не выходят».

В сочетании с философским пониманием проблем CSS, которое закладывалось в предыдущем руководстве, этот текст даёт полный набор инструментов для осознанного использования @scope. Дальше — только практика.

Попробуйте написать первый @scope уже сегодня. Возьмите проблемный компонент в текущем проекте и посмотрите, станет ли код проще и надёжнее. Скорее всего — да.

Комментарии


Дополнительные материалы

Предыдущая Статья

Модальное окно или диалог: как выбрать и не сломать доступность