Ограничьте охват селекторов с помощью правила CSS @scope. Ограничьте охват селекторов с помощью правила CSS @scope.

Узнайте, как использовать @scope для выбора элементов только в пределах ограниченного поддерева вашего DOM.

Browser Support

  • Хром: 118.
  • Край: 118.
  • Firefox: за флагом.
  • Сафари: 17.4.

Source

Тонкое искусство написания селекторов CSS

При написании селекторов вы можете оказаться разрывающимся между двумя мирами. С одной стороны, вам нужно быть очень конкретным в отношении того, какие элементы вы выбираете. С другой стороны, вы хотите, чтобы ваши селекторы легко переопределялись и не были тесно связаны со структурой DOM.

Например, если вы хотите выбрать «главное изображение в области содержимого компонента карты» (а это довольно специфический выбор элемента), вам, скорее всего, не захочется писать селектор типа .card > .content > img.hero .

  • Этот селектор имеет довольно высокую специфичность (0,3,1) , что затрудняет его переопределение по мере роста вашего кода.
  • Полагаясь на прямой дочерний комбинатор, он тесно связан со структурой DOM. Если разметка когда-либо изменится, вам также необходимо изменить CSS.

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

Найти правильный баланс в этом часто бывает довольно непросто. За прошедшие годы некоторые разработчики придумали решения и обходные пути, которые помогут вам в подобных ситуациях. Например:

  • Такие методологии, как БЭМ, требуют, чтобы вы присваивали этому элементу класс card__img card__img--hero , чтобы сохранить низкую специфичность, в то же время позволяя вам быть конкретными в том, что вы выбираете.
  • Решения на основе JavaScript, такие как Scoped CSS или Styled Components, переписывают все ваши селекторы, добавляя случайно сгенерированные строки, такие как sc-596d7e0e-4 , в ваши селекторы, чтобы они не ориентировались на элементы на другой стороне вашей страницы.
  • Некоторые библиотеки даже вообще отменяют селекторы и требуют размещать триггеры стилей непосредственно в самой разметке.

Но что, если вам ничего из этого не нужно? Что, если бы CSS дал вам возможность быть достаточно конкретными в отношении того, какие элементы вы выбираете, не требуя при этом писать селекторы с высокой специфичностью или те, которые тесно связаны с вашим DOM? Что ж, именно здесь в игру вступает @scope , предлагающий вам возможность выбирать элементы только внутри поддерева вашего DOM.

Представляем @scope

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

Например, чтобы настроить таргетинг только на элементы <img> в компоненте .card , вы устанавливаете .card в качестве корня области действия at-правила @scope .

@scope (.card) {
    img {
        border-color: green;
    }
}

Правило ограниченного стиля img { … } может эффективно выбирать только те элементы <img> , которые находятся в области действия соответствующего элемента .card .

Чтобы предотвратить выбор элементов <img> внутри области содержимого карточки ( .card__content ), вы можете сделать селектор img более конкретным. Другой способ сделать это — использовать тот факт, что at-правило @scope также принимает предел области видимости , который определяет нижнюю границу.

@scope (.card) to (.card__content) {
    img {
        border-color: green;
    }
}

Это правило стиля с ограниченной областью действия предназначено только для элементов <img> , которые расположены между элементами .card и .card__content в дереве предков. Этот тип области видимости с верхней и нижней границей часто называют кольцевой областью действия.

Селектор :scope

По умолчанию все правила стиля с ограниченной областью действия относятся к корневой области области действия. Также возможно настроить таргетинг на сам корневой элемент области видимости. Для этого используйте селектор :scope .

@scope (.card) {
    :scope {
        /* Selects the matched .card itself */
    }
    img {
       /* Selects img elements that are a child of .card */
    }
}

Селекторы внутри правил стиля с ограниченной областью действия неявно добавляют к началу :scope . Если хотите, вы можете указать это явно, добавив :scope самостоятельно. В качестве альтернативы вы можете добавить селектор & из CSS Nesting .

@scope (.card) {
    img {
       /* Selects img elements that are a child of .card */
    }
    :scope img {
        /* Also selects img elements that are a child of .card */
    }
    & img {
        /* Also selects img elements that are a child of .card */
    }
}

Ограничение области видимости может использовать псевдокласс :scope , чтобы требовать определенного отношения к корню области видимости:

/* .content is only a limit when it is a direct child of the :scope */
@scope (.media-object) to (:scope > .content) { ... }

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

/* .content is only a limit when the :scope is inside .sidebar */
@scope (.media-object) to (.sidebar :scope .content) { ... }

Обратите внимание, что сами правила стиля с ограниченной областью действия не могут выйти за пределы поддерева. Такие выборы, как :scope + p недействительны, поскольку при этом происходит попытка выбрать элементы, не входящие в область видимости.

@scope и специфика

Селекторы, которые вы используете в прелюдии к @scope не влияют на специфичность содержащихся в них селекторов. В приведенном ниже примере специфичность селектора img по-прежнему равна (0,0,1) .

@scope (#sidebar) {
    img { /* Specificity = (0,0,1) */
        
    }
}

Специфика :scope заключается в том, что он является обычным псевдоклассом, а именно (0,1,0) .

@scope (#sidebar) {
    :scope img { /* Specificity = (0,1,0) + (0,0,1) = (0,1,1) */
        
    }
}

В следующем примере внутренне & перезаписывается в селектор, который используется для корня области видимости, заключенный в селектор :is() . В конце концов, браузер будет использовать :is(#sidebar, .card) img в качестве селектора для сопоставления. Этот процесс известен как обессахаривание .

@scope (#sidebar, .card) {
    & img { /* desugars to `:is(#sidebar, .card) img` */
        
    }
}

Поскольку & очищается с помощью :is() , специфичность & рассчитывается в соответствии с правилами специфичности :is() : специфичность & равна специфичности его наиболее конкретного аргумента.

Применительно к этому примеру специфика :is(#sidebar, .card) связана со спецификой его самого конкретного аргумента, а именно #sidebar , и поэтому он становится (1,0,0) . Объедините это со спецификой img , которая равна (0,0,1) , и вы получите (1,0,1) как специфику для всего сложного селектора.

@scope (#sidebar, .card) {
    & img { /* Specificity = (1,0,0) + (0,0,1) = (1,0,1) */
        
    }
}

Разница между :scope и & внутри @scope

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

По этой причине & можно использовать несколько раз. В этом отличие от :scope , который вы можете использовать только один раз, поскольку вы не можете сопоставить корень области видимости внутри корня области видимости.

@scope (.card) {
  & & { /* Selects a `.card` in the matched root .card */
  }
  :scope :scope { /* ❌ Does not work */
    
  }
}

Объем без прелюдий

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

<div class="card">
  <div class="card__header">
    <style>
      @scope {
        img {
          border-color: green;
        }
      }
    </style>
    <h1>Card Title</h1>
    <img src="…" height="32" class="hero">
  </div>
  <div class="card__content">
    <p><img src="…" height="32"></p>
  </div>
</div>

В приведенном выше примере правила с областью действия нацелены только на элементы внутри div с именем класса card__header , поскольку этот div является родительским элементом элемента <style> .

@scope в каскаде

Внутри CSS Cascade @scope также добавляет новый критерий: область видимости близости . Этот шаг следует за конкретикой, но перед порядком появления.

Визуализация каскада CSS.

Согласно спецификации :

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

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

<style>
    .light { background: #ccc; }
    .dark  { background: #333; }
    .light a { color: black; }
    .dark a { color: white; }
</style>
<div class="light">
    <p><a href="#">What color am I?</a></p>
    <div class="dark">
        <p><a href="#">What about me?</a></p>
        <div class="light">
            <p><a href="#">Am I the same as the first?</a></p>
        </div>
    </div>
</div>

При просмотре этого небольшого фрагмента разметки третья ссылка будет white , а не black , даже если она является дочерним элементом элемента div с примененным к нему классом .light . Это связано с критерием порядка появления, который каскад использует здесь для определения победителя. Он видит, что .dark a был объявлен последним, поэтому он выиграет от правила .light a

Теперь эта проблема решена с помощью критерия близости области действия:

@scope (.light) {
    :scope { background: #ccc; }
    a { color: black;}
}

@scope (.dark) {
    :scope { background: #333; }
    a { color: white; }
}

Поскольку оба a с областью действия имеют одинаковую специфику, в действие вступает критерий близости области действия. Он взвешивает оба селектора по близости к их корню области видимости. Для этого третьего a требуется только один переход к корневому элементу области видимости .light и два к корневому элементу .dark . Поэтому победит a в .light .

Заключительное примечание: изоляция селектора, а не изоляция стиля.

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

@scope (.card) to (.card__content) {
  :scope {
    color: hotpink;
  }
}

В приведенном выше примере элемент .card__content и его дочерние элементы имеют hotpink цвет, поскольку они наследуют значение от .card .

(Фото на обложке Рустама Бурханова на Unsplash )