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

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

Опубликовано: 4 октября 2023 г.

Browser Support

  • Chrome: 118.
  • Край: 118.
  • Firefox: 146.
  • Сафари: 17.4.

Source

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

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

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

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

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

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

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

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

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

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

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

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

Чтобы предотвратить выбор элементов <img> внутри области содержимого карточки ( .card__content ), можно сделать селектор img более специфичным. Другой способ сделать это — использовать тот факт, что правило @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 . При объявлении этого свойства внутри области видимости `donut` color по-прежнему наследует дочерние элементы внутри отверстия `donut`.

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

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