Saiba como usar @scope para selecionar elementos apenas em uma subárvore limitada do DOM.
Publicado em: 4 de outubro de 2023
Ao escrever seletores, você pode ficar dividido entre dois mundos. Por um lado, você quer ser bem específico sobre quais elementos selecionar. Por outro lado, você quer que seus seletores permaneçam fáceis de substituir e não sejam fortemente acoplados à estrutura do DOM.
Por exemplo, quando você quer selecionar "a imagem principal na área de conteúdo do componente de card", que é uma seleção de elemento bastante específica, provavelmente não quer escrever um seletor como .card > .content > img.hero.
- Esse seletor tem uma especificidade bastante alta de
(0,3,1), o que dificulta a substituição à medida que o código cresce. - Ao depender do combinador de filhos diretos, ele fica fortemente acoplado à estrutura do DOM. Se a marcação mudar, você também precisará mudar o CSS.
Mas você também não quer escrever apenas img como seletor desse elemento, porque isso selecionaria todos os elementos de imagem na página.
Encontrar o equilíbrio certo é um desafio. Ao longo dos anos, alguns desenvolvedores criaram soluções e alternativas para ajudar você em situações como essas. Exemplo:
- Metodologias como BEM determinam que você dê a esse elemento uma classe de
card__img card__img--heropara manter a especificidade baixa, permitindo que você seja específico no que seleciona. - Soluções baseadas em JavaScript, como CSS com escopo ou Componentes estilizados, reescrevem todos os seus seletores adicionando strings geradas aleatoriamente, como
sc-596d7e0e-4, para evitar que eles segmentem elementos do outro lado da página. - Algumas bibliotecas até mesmo eliminam os seletores e exigem que você coloque os gatilhos de estilização diretamente na própria marcação.
Mas e se você não precisasse de nada disso? E se o CSS oferecesse uma maneira de ser bem específico sobre quais elementos selecionar, sem exigir que você escreva seletores de alta especificidade ou que estejam fortemente acoplados ao seu DOM? É aí que o @scope entra em ação, oferecendo uma maneira de selecionar elementos apenas em uma subárvore do seu DOM.
Apresentação do @scope
Com @scope, você pode limitar o alcance dos seus seletores. Para isso, defina a raiz de escopo, que determina o limite superior da subárvore que você quer segmentar. Com uma raiz de escopo definida, as regras de estilo contidas, chamadas de regras de estilo com escopo, só podem selecionar dessa subárvore limitada do DOM.
Por exemplo, para segmentar apenas os elementos <img> no componente .card, defina .card como a raiz de escopo da regra-at @scope.
@scope (.card) {
img {
border-color: green;
}
}
A regra de estilo com escopo img { … } só pode selecionar elementos <img> que estão no escopo do elemento .card correspondente.
Para evitar que os elementos <img> dentro da área de conteúdo do card (.card__content) sejam selecionados, você pode tornar o seletor img mais específico. Outra maneira de fazer isso é usar o fato de que a regra @@scope também aceita um limite de escopo, que determina o limite inferior.
@scope (.card) to (.card__content) {
img {
border-color: green;
}
}
Essa regra de estilo com escopo segmenta apenas elementos <img> que estão entre elementos .card e .card__content na árvore de ancestrais. Esse tipo de escopo, com um limite superior e inferior, é chamado de escopo de donut.
O seletor :scope
Por padrão, todas as regras de estilo no escopo são relativas à raiz do escopo. Também é possível segmentar o próprio elemento raiz de escopo. Para isso, use o seletor :scope.
@scope (.card) {
:scope {
/* Selects the matched .card itself */
}
img {
/* Selects img elements that are a child of .card */
}
}
Os seletores dentro das regras de estilo com escopo recebem implicitamente o prefixo :scope. Se quiser, você pode ser explícito sobre isso, adicionando :scope por conta própria.
Outra opção é adicionar o seletor &, de
aninhamento de CSS.
@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 */
}
}
Um limite de escopo pode usar a pseudoclasse :scope para exigir uma relação específica com a raiz de escopo:
/* .content is only a limit when it is a direct child of the :scope */
@scope (.media-object) to (:scope > .content) { ... }
Um limite de escopo também pode fazer referência a elementos fora da raiz usando :scope. Exemplo:
/* .content is only a limit when the :scope is inside .sidebar */
@scope (.media-object) to (.sidebar :scope .content) { ... }
As próprias regras de estilo com escopo não podem escapar da subárvore. Seleções como :scope + p são inválidas porque tentam selecionar elementos que não estão no escopo.
@scope e especificidade
Os seletores usados no prelúdio para @scope não afetam a especificidade dos seletores contidos. No nosso exemplo, a especificidade do seletor img ainda é (0,0,1).
@scope (#sidebar) {
img { /* Specificity = (0,0,1) */
...
}
}
A especificidade de :scope é a de uma pseudoclasse regular, ou seja, (0,1,0).
@scope (#sidebar) {
:scope img { /* Specificity = (0,1,0) + (0,0,1) = (0,1,1) */
...
}
}
No exemplo a seguir, internamente, o & é reescrito para o seletor usado na raiz do escopo, envolvido em um seletor :is(). No final, o navegador vai usar :is(#sidebar, .card) img como seletor para fazer a correspondência. Esse processo é conhecido como desaçucaramento.
@scope (#sidebar, .card) {
& img { /* desugars to `:is(#sidebar, .card) img` */
...
}
}
Como & é desaçucarado usando :is(), a especificidade de & é calculada seguindo as regras de especificidade de :is(): a especificidade de & é a do argumento mais específico.
Aplicada a este exemplo, a especificidade de :is(#sidebar, .card) é a do argumento mais específico, ou seja, #sidebar, e, portanto, se torna (1,0,0). Combine isso com a especificidade de img, que é (0,0,1), e você terá (1,0,1) como a especificidade de todo o seletor complexo.
@scope (#sidebar, .card) {
& img { /* Specificity = (1,0,0) + (0,0,1) = (1,0,1) */
...
}
}
A diferença entre :scope e & em @scope
Além das diferenças no cálculo da especificidade, outra diferença entre :scope e & é que :scope representa a raiz de escopo correspondente, enquanto & representa o seletor usado para corresponder à raiz de escopo.
Por isso, é possível usar & várias vezes. Isso é diferente de :scope, que só pode ser usado uma vez, já que não é possível corresponder a uma raiz de escopo dentro de outra.
@scope (.card) {
& & { /* Selects a `.card` in the matched root .card */
}
:scope :scope { /* ❌ Does not work */
…
}
}
Escopo sem prelúdio
Ao escrever estilos inline com o elemento <style>, é possível definir o escopo das regras de estilo para o elemento pai de inclusão do elemento <style> sem especificar uma raiz de escopo. Para isso, omita o prelúdio da @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>
No exemplo acima, as regras de escopo têm como destino apenas elementos dentro do div com o nome de classe card__header, porque esse div é o elemento pai do elemento <style>.
@scope na cascata
Dentro da cascata CSS, o @scope também adiciona um novo critério: proximidade de escopo. Essa etapa vem depois da especificidade, mas antes da ordem de aparência.
Conforme especificação:
Ao comparar declarações que aparecem em regras de estilo com diferentes raízes de escopo, a declaração com o menor número de saltos geracionais ou de elementos irmãos entre a raiz de escopo e o assunto da regra de estilo com escopo vence.
Essa nova etapa é útil ao aninhar várias variações de um componente. Confira este exemplo, que ainda não usa @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>
Ao visualizar essa pequena parte da marcação, o terceiro link será white em vez de black, mesmo que seja um filho de um div com a classe .light aplicada a ele. Isso ocorre devido ao critério de ordem de aparência que a cascata usa aqui para determinar o vencedor. Ele vê que .dark a foi declarado por último, então vai vencer pela regra .light a.
Com o critério de proximidade de escopo, isso foi resolvido:
@scope (.light) {
:scope { background: #ccc; }
a { color: black;}
}
@scope (.dark) {
:scope { background: #333; }
a { color: white; }
}
Como os dois seletores a com escopo têm a mesma especificidade, o critério de proximidade de escopo entra em ação. Ele pondera os dois seletores por proximidade à raiz de escopo. Para esse terceiro elemento a, há apenas um salto para a raiz de escopo .light, mas dois para a .dark. Portanto, o seletor a em .light vai vencer.
Isolamento de seletor, não de estilo
Saiba que @scope limita o alcance dos seletores. Ele não oferece isolamento de estilo. As propriedades que são herdadas pelos filhos ainda são herdadas, além do limite inferior do @scope. Uma dessas propriedades é a color. Ao declarar um dentro de um escopo de rosca, o color ainda é herdado para
filhos dentro do buraco da rosca.
@scope (.card) to (.card__content) {
:scope {
color: hotpink;
}
}
No exemplo, o elemento .card__content e os filhos dele têm uma cor hotpink porque herdam o valor de .card.