Limitar o alcance dos seletores com o CSS @scope at-rule

Saiba como usar @scope para selecionar elementos apenas em uma subárvore limitada do seu DOM.

Compatibilidade com navegadores

  • 118
  • 118
  • x
  • x

A delicada arte de criar seletores de CSS

Ao escrever seletores, você pode se encontrar dividido entre dois mundos. Por um lado, você quer ser bastante específico sobre quais elementos selecionar. Por outro lado, os seletores devem permanecer fáceis de substituir e não estar rigidamente acoplados à estrutura DOM.

Por exemplo, quando você quer selecionar "a imagem principal na área de conteúdo do componente do cartão", que é uma seleção de elemento bastante específica, provavelmente não quer criar um seletor como .card > .content > img.hero.

  • Esse seletor tem uma especificidade muito alta de (0,3,1), o que dificulta a substituição à medida que o código cresce.
  • Ao depender do combinador filho direto, ele está rigidamente acoplado à estrutura DOM. Se a marcação for alterada, será necessário alterar o CSS também.

Você também não quer escrever apenas img como o seletor desse elemento, porque isso selecionaria todos os elementos de imagem da página.

Muitas vezes, encontrar o equilíbrio certo é um grande 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--hero para manter a especificidade baixa e ser específico na seleção.
  • As soluções baseadas em JavaScript, como CSS com escopo ou Componentes estilizados, reescrevem todos os seletores adicionando strings geradas aleatoriamente, como sc-596d7e0e-4, aos seletores para evitar que eles segmentem elementos do outro lado da página.
  • Algumas bibliotecas até abolizam seletores e exigem que você coloque os acionadores de estilo diretamente na própria marcação.

Mas e se você não precisar de nenhum deles? E se o CSS oferecesse uma maneira de ser bastante específico sobre quais elementos selecionar, sem exigir que você escreva seletores de alta especificidade ou aqueles que estão rigidamente acoplados ao seu DOM? É aqui que a @scope entra em cena, oferecendo uma maneira de selecionar elementos apenas em uma subárvore do seu DOM.

Conheça o @scope

Com @scope, você pode limitar o alcance dos seletores. Para isso, defina a raiz do escopo que determina o limite superior da subárvore que você quer segmentar. Com um conjunto raiz de escopo, as regras de estilo contidas (chamadas de regras de estilo com escopo) só podem ser selecionadas nessa subárvore limitada do DOM.

Por exemplo, para segmentar apenas os elementos <img> no componente .card, defina .card como a raiz do escopo da regra @scope.

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

A regra de estilo com escopo img { … } só pode selecionar apenas elementos <img> que estão no escopo do elemento .card correspondente.

.

Para impedir 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 é usando o fato de que a at-rule do @scope também aceita um limite de escopo, que determina o limite mínimo.

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

Essa regra de estilo com escopo segmenta apenas elementos <img> que estão colocados entre os elementos .card e .card__content na árvore de ancestrais. Esse tipo de escopo, com limites superior e inferior, costuma ser chamado de escopo de rosca.

.

O seletor :scope

Por padrão, todas as regras de estilo com escopo são relativas à raiz do escopo. Também é possível segmentar o próprio elemento raiz do 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 :scope como prefixo. Se quiser, inclua :scope antes do início para deixar claro. Como alternativa, você pode prefixar o seletor & em 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) { ... }

As regras de estilo com escopo não podem escapar da subárvore. Seleções como :scope + p são inválidas porque ela tenta 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 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 normal, ou seja, (0,1,0).

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

No exemplo abaixo, internamente, o & é reescrito no seletor usado para a raiz do escopo, encapsulada em um seletor :is(). No final, o navegador usará :is(#sidebar, .card) img como o seletor para fazer a correspondência. Esse processo é conhecido como simplificação.

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

Como & é simplificado usando :is(), a especificidade de & é calculada de acordo com as regras de especificidade de :is(): a especificidade de & é aquela 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 o resultado será (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 no cálculo da especificidade, outra diferença entre :scope e & é que :scope representa a raiz do escopo correspondente, enquanto & representa o seletor usado para corresponder à raiz do escopo.

Por isso, é possível usar & várias vezes. Isso é diferente de :scope, que você pode usar apenas uma vez, já que não é possível corresponder uma raiz de escopo dentro de uma raiz de escopo.

@scope (.card) {
  & & { /* Selects a `.card` in the matched root .card */
  }
  :root :root { /* ❌ 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 incluído no elemento <style> sem especificar uma raiz do escopo. Para isso, basta omitir 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 com escopo segmentam apenas elementos dentro da div com o nome de classe card__header, porque esse div é o elemento pai do elemento <style>.

.

@scope na cascata

Dentro da Cascade CSS, @scope também adiciona um novo critério: proximidade do escopo. A etapa vem depois da especificidade, mas antes da ordem de aparição.

Visualização da Cascade do CSS.

De acordo com 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 elementos geracionais ou irmãos pula entre a raiz do escopo e o assunto da regra de estilo com escopo.

Essa nova etapa é útil ao aninhar diversas 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 uma div com a classe .light aplicada a ele. Isso se deve à ordem de critério de aparecimento que a cascata usa aqui para determinar o vencedor. Ele vê que a .dark a foi declarada por último, então ela vence da 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 do escopo entra em ação. Ela pesa os dois seletores por proximidade em relação à raiz do escopo. Para esse terceiro elemento a, é apenas um salto para a raiz do escopo de .light, mas dois para o .dark. Portanto, o seletor a da .light vence.

Observação de encerramento: isolamento de seletor, não isolamento de estilo

Uma observação importante a ser feita é que @scope limita o alcance dos seletores, ele não oferece isolamento de estilo. Propriedades herdadas para filhos ainda vão ser herdadas, além do limite inferior de @scope. Uma dessas propriedades é a color. Ao declarar um elemento dentro de um escopo de rosca, o color ainda herdará para filhos dentro do orifício do anel.

@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 da capa de rustam burkhanov no Unsplash)