Как писать CSS с @scope: Полный разбор синтаксиса, изоляция стилей и donut scoping
@scope. Это означает, что технология получила статус Baseline Newly Available и готова к промышленному использованию во всех современных браузерах без исключений.Введение
В предыдущем материале — «Полное руководство по CSS @scope: Изоляция стилей без BEM и Tailwind» — подробно разобрали, почему @scope стал необходим индустрии: как он решает проблемы глобальности CSS, упрощает BEM-структуры и меняет правила игры со специфичностью.
Задача этой статьи — другая.
Не будем повторно сравнивать @scope с методологиями или обсуждать философию изоляции стилей. Вместо этого сфокусируемся на сугубо практических вопросах:
- Как именно пишется
@scopeв разных контекстах? - Чем отличается работа внутри CSS-файла от
<style>-блока в HTML? - Что означают все части синтаксиса — от корня скоупа до «донат-скоупинга»?
- Где какой подход применять, чтобы не навредить производительности и поддерживаемости?
Этот текст задуман как прямая техническая проекция предыдущего руководства. Если там мы отвечали на вопрос «зачем», здесь — на вопрос «как». Материалы можно читать отдельно, но вместе они дают полную картину: от понимания проблемы до готовых синтаксических конструкций, которые можно копировать в свой код уже сегодня.
Перейдём к анатомии @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;
}
}Что происходит в этом примере:
- Корень скоупа:
.navigation— все ссылки внутри навигации потенциально могут попасть под действие правил. - Предел скоупа:
.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);
}
}Когда выбирать этот способ:
- Стили переиспользуются на многих страницах
- У вас настроена система сборки и кэширования CSS
- Важен строгий контроль версий и разделение ответственности (CSS отдельно от HTML)
Плюсы:
- DRY (Don‘t Repeat Yourself): стили написаны один раз и подключаются везде, где нужны
- Кэширование браузером: внешний CSS-файл загружается один раз при первом посещении
- Привычный workflow: не требует изменения процессов разработки
Минусы:
- Рендер-блокирующий ресурс: браузер не покажет страницу, пока не загрузит и не обработает CSS
- Риск CLS: если вы грузите CSS асинхронно, чтобы избежать блокировки рендера, появляется вероятность смещения макета (Cumulative Layout Shift)
- Разделение сущностей: HTML и CSS живут в разных файлах, что при активной разработке может замедлять внесение правок
Этот способ идеально подходит для базовых стилей, библиотек компонентов и всего, что не меняется от страницы к странице.
В <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>. Стили применяются только к содержимому этой секции и не влияют на остальную страницу.
Когда выбирать этот способ:
- Компонент используется на странице один раз (например, лендинг или уникальный блок)
- Критически важна производительность загрузки (нет лишних HTTP-запросов)
- Вы пишете прототип или MVP, где скорость важнее архитектурной чистоты
Плюсы:
- Нет рендер-блокировки: стили приходят вместе с HTML, браузеру не нужно ждать загрузки внешнего файла
- Нет CLS: стили применяются сразу при парсинге разметки
- Локальность: код компонента полностью самодостаточен (HTML + CSS в одном фрагменте)
Минусы:
- Нарушение DRY: если тот же компонент понадобится на другой странице, стили придётся копировать
- Нет кэширования: при переходе между страницами стили будут загружаться заново в составе HTML
- Сложность поддержки: при изменении стиля нужно править все страницы, где используется компонент
Этот способ отлично работает для одноразовых акций, посадочных страниц и ситуаций, где критична каждая миллисекунда загрузки.
Комбинированный подход
В реальных проектах редко можно обойтись чем-то одним. Наиболее прагматичная стратегия — комбинировать оба подхода, используя сильные стороны каждого.
Типичная архитектура:
- Глобальный CSS (внешний файл) — для базовых стилей, типографики, цветовой схемы, общих компонентов, которые используются повсеместно.
- Секционные стили (внешние файлы, подключаемые по необходимости) — для крупных разделов сайта (например,
catalog.css,checkout.css), которые подключаются только на соответствующих страницах через условную логику в шаблоне. - Локальные
@scopeв<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;
}Зачем нужны три дублирующих друг друга селектора? Исторически сложилось:
html— самый старый и очевидный способ:root— появился из CSS3 и работает не только в HTML, но и в SVG, XML:scope— добавлен для симметрии с поведением внутри@scopeи для согласованности с DOM-методами
На практике в глобальных стилях нет причин использовать :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 > * не работает так, как я ожидаю?
Самая частая причина — неправильное понимание границ скоупа. Проверьте:
- Действительно ли элемент, который вы хотите выбрать, находится внутри корня скоупа?
- Не попадает ли он в зону действия предела (
to ...), если вы его использовали? - Нет ли другого, более близкого скоупа, который переопределяет стили?
Если проблема не в этом, проверьте, не перебивается ли ваше правило другим с той же специфичностью, но из более близкого скоупа.
Влияет ли @scope на производительность?
Влияние на производительность рендеринга минимально и в подавляющем большинстве случаев неотличимо от обычных селекторов. Браузеры оптимизируют обработку скоупов так же, как и любых других правил. Гораздо большее влияние на производительность оказывает способ подключения CSS (внешний файл против встроенного), что мы подробно разбирали в разделе про три лица @scope.
Можно ли использовать @scope с препроцессорами (Sass, Less)?
Да, можно. Все современные препроцессоры корректно обрабатывают @scope как нативное CSS-правило и не изменяют его. Однако внутри @scope вы можете использовать возможности препроцессора — переменные, миксины, вложенность — точно так же, как и в любом другом месте.
Где посмотреть реальные примеры кода?
В этой статье все примеры самодостаточны и готовы к копированию. Кроме того, в разделе «Типичный сценарий: день из жизни поддержки CSS» из первого руководства показана эволюция реального кода от проблемной реализации к чистому решению с @scope.
Заключение и дорожная карта
Мы проделали большой путь: от базового синтаксиса @scope до нюансов его применения в разных контекстах и даже заглянули в поведение :scope в JavaScript. Пришло время собрать все части воедино и ответить на главный практический вопрос: с чего начать внедрение @scope в проект уже завтра утром?
Что мы теперь знаем
- Синтаксис
@scopeсостоит из корня (обязательно) и предела (опционально). Предел создаёт «донат-скоупинг» — область действия в виде кольца, что позволяет элегантно обходить вложенные компоненты. - Внутри скоупа можно использовать
:scopeи `& для явного указания на корневой элемент — это повышает читаемость и даёт единообразие. - Контекст применения радикально меняет характеристики кода:
- Внешние CSS-файлы — переиспользуемость и кэширование.
- Встроенные
<style>-блоки — скорость загрузки и изоляция. - Комбинированный подход — прагматичный баланс для реальных проектов.
:scopeживёт и вне@scope— в DOM-методах он незаменим для выборки прямых потомков, а в глобальных стилях дублирует:root(что полезно знать, но редко нужно использовать).
Дорожная карта внедрения
@scope не требует мгновенного рефакторинга всей кодовой базы. Это инструмент для постепенного улучшения. Вот реалистичный план из трёх этапов:
Этап 1. Разведка (одна-две итерации)
Выберите изолированный компонент, который сейчас:
- Требует сложных селекторов для переопределения стилей
- Страдает от утечек стилей в соседние блоки
- Используется только на одной странице (идеальный кандидат для встроенного
<style>)
Перепишите его стили с @scope. Убедитесь, что поведение предсказуемо во всех целевых браузерах. Сравните объём кода и читаемость с исходным вариантом.
Этап 2. Локальный стандарт (следующие несколько недель)
Если эксперимент удался, зафиксируйте решение в команде:
- Добавьте раздел про
@scopeв руководство по стилю кодирования - Определите, в каких случаях вы предпочитаете внешние файлы, а в каких — встроенные блоки
Начните использовать @scope во всех новых компонентах, которые естественно попадают под его применение
Этап 3. Стратегический рефакторинг (по мере необходимости)
Постепенно, при плановых доработках существующих компонентов, заменяйте старые конструкции с каскадными войнами и !important на чистые @scope-блоки. Это не должно быть отдельной задачей — просто улучшайте код при каждом касании.
Вместо послесловия
@scope — не серебряная пуля и не замена методологиям вроде BEM или инструментам вроде Tailwind. Это новый примитив языка, который закрывает брешь, существовавшую двадцать лет. Теперь появился нативный способ сказать браузеру: «вот здесь начинается компонент, а здесь заканчивается, и за эти границы стили не выходят».
В сочетании с философским пониманием проблем CSS, которое закладывалось в предыдущем руководстве, этот текст даёт полный набор инструментов для осознанного использования @scope. Дальше — только практика.
Попробуйте написать первый @scope уже сегодня. Возьмите проблемный компонент в текущем проекте и посмотрите, станет ли код проще и надёжнее. Скорее всего — да.