Limita el alcance de tus selectores con el permiso at-rule de CSS @scope.

Aprende a usar @scope para seleccionar elementos solo en un subárbol limitado de tu DOM.

Navegadores compatibles

  • Chrome: 118
  • Edge: 118.
  • Firefox: detrás de una marca.
  • Safari: 17.4.

Origen

El delicado arte de escribir selectores CSS

Al escribir selectores, puede que deba alternar entre dos mundos. Por un lado, debes ser bastante específico sobre los elementos que seleccionas. Por otro lado, te conviene que tus selectores sigan siendo fáciles de anular y que no estén estrechamente vinculados con la estructura del DOM.

Por ejemplo, si quieres seleccionar "la imagen hero en el área de contenido del componente de la tarjeta", que es una selección de elementos bastante específica, es probable que no quieras escribir un selector como .card > .content > img.hero.

  • Este selector tiene una especificidad bastante alta de (0,3,1), lo que dificulta la anulación a medida que crece tu código.
  • Al confiar en el combinador secundario directo, se acopla estrechamente con la estructura del DOM. Si el lenguaje de marcado cambia, también debes cambiar la CSS.

Sin embargo, tampoco te recomendamos que escribas solo img como selector para ese elemento, ya que se seleccionarían todos los elementos de imagen de tu página.

Encontrar el equilibrio adecuado en todas estas situaciones suele ser todo un desafío. A lo largo de los años, algunos desarrolladores han ideado soluciones y alternativas para ayudarte en situaciones como estas. Por ejemplo:

  • Las metodologías como BEM dictan que se le otorgue a ese elemento una clase de card__img card__img--hero para mantener la especificidad baja y, al mismo tiempo, permitir que sea específico en lo que seleccione.
  • Las soluciones basadas en JavaScript, como CSS con alcance o Componentes con estilo reescriben todos tus selectores agregando strings generadas de forma aleatoria (como sc-596d7e0e-4) a tus selectores para evitar que se orienten a elementos del otro lado de tu página.
  • Algunas bibliotecas incluso abolen los selectores por completo y requieren que coloques los activadores de estilo directamente en el lenguaje de marcado.

Pero ¿y si no los necesitas? ¿Qué pasaría si CSS te diera una manera de ser bastante específico respecto de los elementos que seleccionas, sin necesidad de escribir selectores de alta especificidad o que estén estrechamente acoplados a tu DOM? Aquí es donde @scope entra en juego, y te ofrece una manera de seleccionar elementos solo dentro de un subárbol de tu DOM.

Presentamos @scope

Con @scope, puedes limitar el alcance de tus selectores. Para ello, se debe configurar la raíz de alcance, que determina el límite superior del subárbol al que se quiere orientar. Con un conjunto raíz de alcance, las reglas de estilo contenidas, denominadas reglas de estilo con alcance, solo pueden seleccionar de ese subárbol limitado del DOM.

Por ejemplo, para apuntar solo a los elementos <img> en el componente .card, debes establecer .card como la raíz de alcance de la regla at @scope.

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

La regla de estilo con alcance img { … } solo puede seleccionar efectivamente elementos <img> que estén dentro del alcance del elemento .card coincidente.

Para evitar que se seleccionen los elementos <img> dentro del área de contenido de la tarjeta (.card__content), puedes hacer que el selector img sea más específico. Otra forma de hacerlo es aprovechar el hecho de que la regla at-@scope también acepta un límite de alcance que determina el límite inferior.

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

Esta regla de diseño con alcance solo se orienta a elementos <img> ubicados entre los elementos .card y .card__content en el árbol principal. Este tipo de alcance, con límites inferior y superior, suele denominarse alcance de anillo

El selector :scope

De forma predeterminada, todas las reglas de estilo con alcance están relacionadas con la raíz de alcance. También es posible apuntar al propio elemento raíz de alcance. Para ello, usa el selector :scope.

@scope (.card) {
    :scope {
        /* Selects the matched .card itself */
    }
    img {
       /* Selects img elements that are a child of .card */
    }
}

Los selectores dentro de las reglas de estilo con permiso tienen el prefijo :scope de manera implícita. Si lo deseas, puedes agregar información explícita al anteponer :scope. Como alternativa, puedes anteponer el selector & desde Anidamiento 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 */
    }
}

Un límite de alcance puede usar la seudoclase :scope para requerir una relación específica con la raíz de alcance:

/* .content is only a limit when it is a direct child of the :scope */
@scope (.media-object) to (:scope > .content) { ... }

Un límite de alcance también puede hacer referencia a elementos fuera de su raíz de alcance mediante :scope. Por ejemplo:

/* .content is only a limit when the :scope is inside .sidebar */
@scope (.media-object) to (.sidebar :scope .content) { ... }

Ten en cuenta que las reglas de diseño con alcance no pueden escapar del subárbol. Las selecciones como :scope + p no son válidas porque intenta seleccionar elementos que no están dentro del alcance.

@scope y especificidad

Los selectores que usas en el preludio de @scope no afectan la especificidad de los selectores contenidos. En el siguiente ejemplo, la especificidad del selector de img sigue siendo (0,0,1).

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

La especificidad de :scope es la de una seudoclase normal, es decir, (0,1,0).

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

En el siguiente ejemplo, de manera interna, el & se reescribe en el selector que se usa para la raíz de alcance, unido dentro de un selector :is(). Al final, el navegador usará :is(#sidebar, .card) img como selector para buscar coincidencias. Este proceso se conoce como expansión de sintaxis.

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

Debido a que la expansión de sintaxis de & se realiza con :is(), la especificidad de & se calcula según las reglas de especificidad de :is(): la especificidad de & es la de su argumento más específico.

En este ejemplo, la especificidad de :is(#sidebar, .card) es la de su argumento más específico, es decir, #sidebar, y, por lo tanto, se convierte en (1,0,0). Combínalo con la especificidad de img, que es (0,0,1), y obtendrás (1,0,1) como la especificidad de todo el selector complejo.

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

Diferencia entre :scope y & dentro de @scope

Además de las diferencias en cómo se calcula la especificidad, otra diferencia entre :scope y & es que :scope representa la raíz de alcance coincidente, mientras que & representa el selector que se usa para hacer coincidir la raíz de alcance.

Debido a esto, es posible usar & varias veces. Esto contrasta con :scope, que solo puedes usar una vez, ya que no puedes hacer coincidir una raíz de alcance dentro de otra raíz de alcance.

@scope (.card) {
  & & { /* Selects a `.card` in the matched root .card */
  }
  :scope :scope { /* ❌ Does not work */
    …
  }
}

Alcance sin introducción

Cuando escribes estilos intercalados con el elemento <style>, puedes definir el alcance de las reglas de estilo en el elemento superior adjunto del elemento <style> si no especificas ninguna raíz de alcance. Para ello, omite el preludio 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>

En el ejemplo anterior, las reglas con alcance solo se orientan a los elementos dentro de la div con el nombre de clase card__header, porque div es el elemento superior del elemento <style>.

@scope en la cascada

Dentro de la Cascada de CSS, @scope también agrega un criterio nuevo: proximidad de alcance. El paso viene después de la especificidad, pero antes del orden de aparición.

Visualización de la cascada de CSS.

Según según las especificaciones:

Cuando se comparan declaraciones que aparecen en reglas de estilo con diferentes raíces de alcance, gana la declaración con la menor cantidad de saltos generacionales o de elementos del mismo nivel entre la raíz de alcance y el sujeto de la regla de estilo con alcance.

Este nuevo paso resulta útil cuando se anidan diferentes variaciones de un componente. Tomemos este ejemplo, que todavía no 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>

Cuando veas ese fragmento de lenguaje de marcado, el tercer vínculo será white en lugar de black, aunque sea un elemento secundario de un div con la clase .light aplicada. Esto se debe al criterio de orden de aparición que la cascada utiliza aquí para determinar al ganador. Ve que .dark a se declaró en último lugar, por lo que ganará de la regla .light a

Con el criterio de proximidad de alcance, esto ahora se resuelve:

@scope (.light) {
    :scope { background: #ccc; }
    a { color: black;}
}

@scope (.dark) {
    :scope { background: #333; }
    a { color: white; }
}

Como ambos selectores a con alcance tienen la misma especificidad, se activa el criterio de proximidad de alcance. Pondera ambos selectores por proximidad a su raíz de alcance. Para ese tercer elemento a, solo hay un salto a la raíz de alcance de .light, pero dos a la .dark. Por lo tanto, ganará el selector a de .light.

Nota de cierre: Aislamiento del selector, no aislamiento del estilo

Nota importante que debes tener en cuenta es que @scope limita el alcance de los selectores, no ofrece aislamiento de estilo. Las propiedades que se heredan a elementos secundarios aún se heredarán, más allá del límite inferior de @scope. Una de esas propiedades es la de color. Cuando declares que uno está dentro del alcance de un anillo, color se heredará de todos modos a los elementos secundarios dentro del agujero de la rosquilla.

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

En el ejemplo anterior, el elemento .card__content y sus elementos secundarios tienen un color hotpink porque heredan el valor de .card.

(Foto de portada de rustam burkhanov en Unsplash)