Escopo Nativo em CSS com @scope
O @scope nativo de CSS limita seletores a um componente ou limite donut, com cascata por proximidade e sem build.
@scope é uma at-rule do CSS que restringe onde um bloco de seletores pode corresponder, a partir de um elemento raiz até um limite inferior opcional, sem etapa de build e sem adicionar especificidade. O MDN lhe atribui o status Baseline “newly available” (recém-disponível), datado de março de 2026.
Se você mantém uma base de código de componentes, já paga por contenção de estilos em algum lugar: uma convenção de nomenclatura que você impõe em code review, um plugin de bundler que gera hashes para nomes de classe, ou um runtime que injeta tags de estilo. Cada um desses recursos existe porque um seletor descendente simples alcança longe demais, e um seletor filho encadeado solda o CSS a uma forma exata do DOM.
Principais Pontos
- Dentro de um bloco
@scope, um seletor isolado mantém apenas a sua própria especificidade, porque o prefixo implícito é:where(:scope)e:where()contribui com peso zero; escrever:scopeexplicitamente adiciona 0-1-0. @scope (.card) to (.card__content)corresponde a elementos entre o card e seu slot de conteúdo: a raiz é incluída, o elemento-limite e tudo abaixo dele são excluídos.- Quando duas declarações com escopo empatam em especificidade, vence aquela cuja raiz de escopo está a menos saltos no DOM em relação ao elemento, independentemente da ordem no código-fonte.
- A proximidade de escopo é comparada depois de importância, camadas de cascata e especificidade, e antes da ordem no código-fonte, de modo que um seletor sem escopo com especificidade maior ainda sobrescreve uma regra com escopo.
@scopelimita onde os seletores correspondem; ele não impede que propriedades herdadas comocolorfluam além do limite de escopo para dentro da região excluída.
Por Que os Seletores Vazam, e o Que BEM, CSS Modules e CSS-in-JS Fazem a Respeito?
Todo seletor CSS é avaliado contra o documento inteiro, então mirar “a imagem hero neste card” força uma escolha entre um seletor estrutural demais e um amplo demais.
.card > .card__body > img { } /* 0-2-1, breaks when the markup moves */
img { } /* 0-0-1, matches every image on the page */
.card__img { } /* 0-1-0, BEM: a unique name per component */
O BEM resolve o problema de alcance com disciplina de nomenclatura: um nome de bloco único, e cada elemento interno carrega uma classe block__element, de modo que um .title isolado nunca exista. Os CSS Modules automatizam a mesma ideia reescrevendo cada nome de classe para um identificador com hash, local ao arquivo, em tempo de build. As bibliotecas CSS-in-JS fazem isso em tempo de execução ou de compilação, gerando esses identificadores a partir do código do seu componente; o estado atual desse ecossistema é abordado separadamente. O rascunho do CSS Cascade Level 6 detalha como essas ferramentas funcionam nos bastidores: elas marcam cada elemento de um componente com um atributo ou classe identificadora, e então adicionam esse marcador a todos os seletores do arquivo.
| Abordagem | Etapa de build | Limite inferior (donut) | Proximidade na cascata | Especificidade extra |
|---|---|---|---|---|
| BEM | Não | Apenas por convenção de nomenclatura | Não | Nível de classe por nome |
| CSS Modules | Sim | Classes com hash por arquivo | Não | Nível de classe por nome |
| CSS-in-JS | Runtime ou compilação | Classe gerada por componente | Não | Nível de classe por nome |
@scope | Não | Cláusula nativa to (limit) | Sim | Nenhuma vinda da raiz |
O Bloco Básico de Escopo CSS: @scope (.card)
@scope (.card) { img { } } corresponde apenas a elementos <img> que sejam descendentes inclusivos de um .card, e a raiz .card não contribui em nada para a especificidade de img.
<article class="card">
<img src="hero.jpg" alt=""> <!-- matched -->
</article>
<img src="logo.svg" alt=""> <!-- not matched -->
@scope (.card) {
img { border-radius: 8px; } /* specificity 0-0-1 */
:scope { padding: 1rem; } /* specificity 0-1-0, the .card itself */
}
As notas do MDN sobre especificidade dentro de um escopo explicam o mecanismo. Um seletor isolado no bloco é avaliado como se o prefixo :where(:scope) estivesse à sua frente e, como :where() não carrega peso próprio, a raiz não adiciona nada ao total. Isso é o inverso do que fazem as ferramentas de marcação por atributo: um hook gerado como [data-v-abc123] adiciona peso de atributo a cada seletor que toca. Quando escrito explicitamente, :scope é uma pseudo-classe comum e adiciona 0-1-0, então :scope img é 0-1-1.
O Que É um Escopo Donut?
Um escopo donut, escrito como @scope (.card) to (.card__content), estiliza tudo do card para baixo, até — mas sem incluir — .card__content e sua subárvore, que é exatamente o problema de conteúdo em slot que componentes aninhados criam.
<article class="card">
<img src="hero.jpg" alt=""> <!-- in scope -->
<div class="card__content">
<img src="inline.jpg" alt=""> <!-- excluded: below the limit -->
</div>
</article>
@scope (.card) to (.card__content) {
img { border: 4px solid goldenrod; }
}
Por padrão, a própria raiz conta como dentro do escopo e o elemento-limite não. Acrescentar > * a qualquer um dos seletores inverte esse limite. @scope (.card) to (.card__content > *) traz o próprio elemento .card__content para dentro do escopo, ainda excluindo seus filhos, o que é útil quando o wrapper do slot precisa de padding mas o conteúdo encaixado deve permanecer intocado. Na definição do rascunho, um elemento se qualifica quando está na raiz ou abaixo dela, e não é nem está abaixo de um limite. Nenhuma combinação de seletores expressa “descendente de X mas não dentro de Y” sem uma classe de fronteira em cada elemento ou cadeias de :not() que reintroduzem especificidade.
A Proximidade de Escopo Supera a Ordem no Código-Fonte
Quando duas regras com escopo empatam em especificidade, vence a declaração cuja raiz de escopo está mais próxima do elemento, o que corrige o bug de temas aninhados que seletores descendentes simples resolvem errado.
<div class="theme-light">
<p>Light</p>
<div class="theme-dark">
<p>Dark</p>
<div class="theme-light">
<p>Light again?</p>
</div>
</div>
</div>
Com seletores comuns, o parágrafo mais interno corresponde tanto a .theme-light p quanto a .theme-dark p com 0-1-1, então vence a regra que aparecer por último na folha de estilos, e o parágrafo é renderizado na cor do tema escuro apesar de estar dentro de um contêiner claro.
@scope (.theme-light) {
p { color: #1b1b1b; }
}
@scope (.theme-dark) {
p { color: #f2f2f2; } /* declared later, but loses on the inner p */
}
O <p> mais interno está a um salto de sua raiz .theme-light e a dois de .theme-dark, então a regra clara se aplica. O MDN percorre esse mesmo exemplo prático, e, sob a regra de proximidade da especificação, uma regra sem raiz de escopo nunca pode vencer essa disputa, porque sua contagem de saltos é considerada infinita.
Onde a Proximidade se Encaixa na Cascata?
A proximidade de escopo é comparada depois de importância, camadas de cascata e especificidade, e antes da ordem no código-fonte, então um seletor sem escopo com especificidade maior ainda sobrescreve uma regra com escopo, não importa o quão próxima esteja a raiz do escopo.
A ordem de classificação do Cascade 6 lista sete critérios em ordem decrescente de precedência:
- Origem e importância
- Contexto (encapsulamento de shadow tree)
- O atributo style
- Camadas de cascata
- Especificidade
- Proximidade de escopo
- Ordem de aparição
A proximidade é um critério de desempate para a especificidade, não um substituto dela:
@scope (aside) {
p { color: green; } /* 0-0-1, scoped */
}
aside#sidebar p { color: red; } /* 1-0-2, unscoped, wins */
<aside id="sidebar"><p>This is red.</p></aside>
Classificar a proximidade abaixo da especificidade foi uma escolha deliberada, e o changelog do rascunho registra a remoção da versão mais forte que a teria colocado acima. O raciocínio aparece no explicador por trás do recurso: se a proximidade vencesse primeiro, a especificidade só resolveria disputas entre seletores de mesma proximidade, de modo que regras escritas para ficar acima ou abaixo umas das outras passariam a ganhar e perder conforme o formato do DOM. Se você espera que estilos com escopo se comportem como o encapsulamento do Shadow DOM, é aqui que essa expectativa falha.
Seletores Têm Escopo; a Herança Não
@scope limita onde um seletor pode corresponder; ele não impede que propriedades herdadas fluam além do limite do escopo, então um color definido na raiz do escopo ainda alcança todos os elementos dentro da região excluída.
<article class="card">
<p>Card text</p>
<div class="card__content">
<p>Slotted text: also hotpink, with no border</p>
</div>
</article>
@scope (.card) to (.card__content) {
:scope { color: hotpink; } /* inherited: crosses the limit */
p { border: 1px solid currentColor; } /* not inherited, and p in the slot is out of scope */
}
O parágrafo no slot está fora do escopo, então nenhum seletor com escopo corresponde a ele e ele não recebe borda. Ainda assim é renderizado em hotpink, porque a herança é um mecanismo em nível de propriedade que roda após a cascata e desconhece escopos. A referência do @scope faz a mesma observação: o escopo delimita quais elementos um seletor pode alcançar, não onde os estilos resultantes acabam. Qualquer coisa que você queira conter na fronteira do slot precisa de um reset explícito no próprio slot, como sempre foi.
Quais Navegadores Suportam @scope, e Quando Você Deve Usá-lo?
O MDN coloca @scope no Baseline “newly available” desde março de 2026, o que significa que todos os principais engines atuais o entregam. Os dados de compatibilidade mostram o primeiro suporte no Chrome e no Edge 118 e no Firefox 146; o Safari 17.4 o lançou, o Safari 26.0 até 26.3 são marcados como parciais, e o Safari 26.4 restaurou o suporte completo. Navegadores que não o possuem descartam a at-rule inteira, como o CSS exige para construções não reconhecidas, de modo que um bloco com escopo degrada para nada em vez de para uma regra quebrada.
Recorra ao @scope quando um componente é dono de uma região da árvore mas não deve estilizar o que é encaixado nela, ou quando o mesmo componente se aninha dentro de si mesmo com variantes diferentes. Deixe-o de lado para resets globais, tipografia e tokens de marca: esses devem fluir livremente, e a cascata é construída para que isso aconteça. Onde você precisa ordenar folhas de estilo inteiras umas em relação às outras em vez de isolar subárvores, as camadas de cascata continuam sendo a ferramenta certa.
O @scope move a contenção do seu pipeline de build para o navegador: fronteiras donut e resolução por proximidade agora são recursos da cascata, não convenções ou hashes gerados. O próximo passo prático é pegar um componente que hoje depende de um sufixo __element ou de uma classe com hash para manter seu slot interno intocado, reescrevê-lo como um bloco @scope (.component) to (.slot), e verificar se qualquer cor ou fonte que você esperava que parasse no slot está sendo explicitamente resetada ali.
Perguntas Frequentes
Posso detectar o suporte a @scope com @supports, e o que acontece em navegadores que não o possuem?
Navegadores sem suporte a @scope descartam o bloco da at-rule, então as regras com escopo não se aplicam e nada mais quebra. O CSS Conditional Rules Level 5 define @supports at-rule(@scope), mas at-rule() é mais recente que @scope (o Chromium 148 foi o primeiro a implementá-lo), então qualquer navegador antigo o suficiente para não ter @scope também não tem a função de detecção. Escreva regras de fallback simples, com especificidade igual ou menor, fora do bloco; os navegadores compatíveis as sobrescreverão com as regras com escopo.
Posso usar @scope sem um seletor raiz?
Sim. Dentro de um elemento style do HTML você pode escrever @scope sem seletor raiz em seu prelúdio, e o navegador delimita as regras contidas ao elemento pai desse elemento style. A forma inline também aceita um limite, escrito como @scope to (.card__content). Isso serve bem a fragmentos renderizados no servidor que trazem seus próprios estilos. Em folhas de estilo comuns, use a forma de prelúdio (root) para que a raiz do escopo seja explícita.
Posso ler regras @scope a partir do JavaScript?
Sim, por meio da interface CSSScopeRule, que estende CSSGroupingRule. Ela expõe duas propriedades de string somente leitura: start retorna o seletor serializado da raiz do escopo e end retorna o seletor do limite do escopo, cada um nulo quando essa parte do prelúdio é omitida. Acesse uma regra através de document.styleSheets e sua lista cssRules, e acesse as regras de estilo com escopo dentro dela pela propriedade cssRules herdada de CSSGroupingRule.
Qual é a diferença entre @scope e Shadow DOM para encapsulamento de estilos?
O Shadow DOM cria uma árvore DOM separada com uma fronteira rígida de estilos: seletores externos não conseguem corresponder dentro de um shadow root, exceto via ::part, e regras internas não conseguem alcançar o exterior. O @scope não muda nada na marcação; ele apenas limita onde os seletores dentro de um bloco podem corresponder, então outras folhas de estilo ainda conseguem mirar todos os elementos do escopo. Propriedades herdadas cruzam ambas as fronteiras. O @scope também não precisa de JavaScript nem de shadow root.
Complete picture for complete understanding
Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.
Star on GitHub12k