Aprenda a usar @scope para selecionar elementos apenas em um subárvore limitada do DOM.
A arte delicada de escrever seletores de CSS
Ao escrever seletores, você pode se sentir dividido entre dois mundos. Por um lado, você quer ser bastante específico sobre quais elementos selecionar. Por outro lado, você quer que os seletores continuem fáceis de substituir e não sejam acoplados à estrutura do DOM.
Por exemplo, quando você quiser selecionar "a imagem principal na área de conteúdo do componente do card", que é uma seleção de elementos bastante específica, provavelmente não vai querer usar um seletor como .card > .content > img.hero
.
- Esse seletor tem uma especificidade de
(0,3,1)
muito alta, o que dificulta a substituição à medida que o código cresce. - Ao depender do combinator filho direto, ele fica intimamente associado à estrutura do DOM. Caso a marcação mude, você também precisará alterar o CSS.
No entanto, também não é recomendável escrever apenas img
como o seletor desse elemento, já que isso selecionaria todos os elementos de imagem na página.
Encontrar o equilíbrio certo geralmente é um grande desafio. Ao longo dos anos, alguns desenvolvedores criaram soluções e alternativas para ajudar em situações como essa. Exemplo:
- Metodologias como a BEM determinam que você atribua a esse elemento uma classe de
card__img card__img--hero
para manter a especificidade baixa, permitindo que você seja específico na seleção. - Soluções baseadas em JavaScript, como CSS com escopo ou componentes estilizados, reescrevem todos os 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 acionadores de estilo diretamente na marcação.
Mas e se você não precisar de nenhuma delas? E se o CSS oferecesse uma maneira de ser bastante específico sobre quais elementos você seleciona, sem precisar escrever seletores de alta especificidade ou que estejam intimamente acoplados ao DOM? É aí que o @scope
entra em ação, oferecendo uma maneira de selecionar elementos apenas em um subárvore do DOM.
Introdução ao @scope
Com @scope
, você pode limitar o alcance dos seus seletores. Para fazer isso, defina a raiz de escopo, que determina o limite superior do subárvore que você quer segmentar. Com um conjunto de raiz de escopo, as regras de estilo contidas, chamadas de regras de estilo com escopo, só podem selecionar esse 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 @scope
.
@scope (.card) {
img {
border-color: green;
}
}
A regra de estilo com escopo img { … }
só pode selecionar elementos <img>
que estejam 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 só tem como alvo elementos <img>
colocados entre elementos .card
e .card__content
na árvore ancestral. Esse tipo de escopo, com limites superior e inferior, é geralmente chamado de escopo de donut.
O seletor :scope
Por padrão, todas as regras de estilo com escopo são relativas à raiz do escopo. Também é possível direcionar 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 de regras de estilo com escopo recebem implicitamente o prefixo :scope
. Se quiser, você pode ser explícito, preenchendo :scope
. Como alternativa, você pode adicionar o seletor &
no início, usando o 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 do 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 referenciar elementos fora da raiz do escopo usando :scope
. Exemplo:
/* .content is only a limit when the :scope is inside .sidebar */
@scope (.media-object) to (.sidebar :scope .content) { ... }
Observe que as 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 de @scope
não afetam a especificidade deles. No exemplo abaixo, 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 para a raiz do escopo, envolvido em um seletor :is()
. No final, o navegador vai usar :is(#sidebar, .card) img
como o seletor para fazer a correspondência. Esse processo é conhecido como dessugaramento.
@scope (#sidebar, .card) {
& img { /* desugars to `:is(#sidebar, .card) img` */
…
}
}
Como &
é simplificado usando :is()
, a especificidade de &
é calculada seguindo as regras de especificidade de :is()
: a especificidade de &
é a do argumento mais específico.
Aplicado 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ê vai 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 &
dentro de @scope
Além das diferenças na forma como a especificidade é calculada, 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, porque não é possível corresponder 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 in-line com o elemento <style>
, é possível definir o escopo das regras de estilo para o elemento pai associado ao elemento <style>
sem especificar nenhuma raiz de escopo. Para fazer isso, omita o prelúdio de @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 só segmentam elementos dentro do div
com o nome de classe card__header
, porque div
é o elemento pai do <style>
.
@scope na cascata
Dentro da cascata CSS, @scope
também adiciona um novo critério: proximidade de escopo. A etapa vem depois da especificidade, mas antes da ordem de aparição.
De acordo com a 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 de elementos irmãos ou de geração 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 marcação, o terceiro link será white
em vez de black
, mesmo que seja filho de um div
com a classe .light
aplicada a ele. Isso ocorre devido ao critério de ordem de aparição que a cascata usa para determinar o vencedor. Ele identifica que .dark a
foi declarado por último, então ele vai vencer de acordo com a regra .light a
Com o critério de proximidade do escopo, isso é 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 pesa os dois seletores pela proximidade da raiz do 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.
Observação final: isolamento do seletor, não do estilo
Uma observação importante a ser feita é que @scope
limita o alcance dos seletores, não oferece isolamento de estilo. As propriedades herdadas para filhos ainda serão herdadas, além do limite inferior de @scope
. Uma dessas propriedades é a color
. Ao declarar esse dentro de um escopo de rosquinha, o color
ainda vai herdar para os filhos dentro do buraco da rosquinha.
@scope (.card) to (.card__content) {
:scope {
color: hotpink;
}
}
No exemplo acima, o elemento .card__content
e os filhos dele têm uma cor hotpink
porque herdam o valor de .card
.
(Foto de capa de rustam burkhanov no Unsplash)