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

Cuando escribas selectores, es posible que te encuentres entre dos mundos. Por un lado, debes ser bastante específico sobre los elementos que seleccionas. Por otro lado, quieres que tus selectores sean fáciles de anular y no estén estrechamente vinculados a 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 su anulación a medida que crece el código.
  • Debido a que se basa en el combinador secundario directo, está estrechamente acoplado a la estructura del DOM. Si el lenguaje de marcado cambia, también debes cambiar el CSS.

Sin embargo, tampoco quieres escribir solo img como selector para ese elemento, ya que eso seleccionaría 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 idearon soluciones y alternativas para ayudarte en situaciones como estas. Por ejemplo:

  • Las metodologías como BEM indican que debes asignarle a ese elemento una clase de card__img card__img--hero para mantener la especificidad baja y, al mismo tiempo, permitirte ser específico en lo que selecciones.
  • 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 eliminan por completo los selectores y requieren que coloques los activadores de diseño directamente en el marcado.

Pero ¿qué sucede si no necesitas ninguno de ellos? ¿Qué pasaría si CSS te brindara una forma de ser bastante específico sobre los elementos que seleccionas, sin necesidad de escribir selectores de alta especificidad o que estén estrechamente vinculados a tu DOM? Bueno, ahí es donde entra en juego @scope, que te ofrece una forma 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, establece la raíz de alcance, que determina el límite superior del subárbol al que deseas segmentar. 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 segmentar solo los elementos <img> en el componente .card, estableces .card como la raíz de alcance de la regla de anidación @scope.

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

La regla de estilo con alcance img { … } solo puede seleccionar de manera eficaz 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 usar el hecho de que la regla de @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 los elementos <img> que se colocan entre los elementos .card y .card__content en el árbol de ancestros. Este tipo de alcance, con un límite superior e inferior, suele denominarse alcance de dona.

El selector :scope

De forma predeterminada, todas las reglas de estilo con alcance son relativas a la raíz del alcance. También es posible segmentar el 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 */
    }
}

A los selectores dentro de las reglas de diseño centradas se les agrega :scope de forma implícita. Si lo deseas, puedes ser explícito y anteponer :scope. Como alternativa, puedes anteponer el selector & desde la anielación 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 estilo con alcance no pueden escapar del subárbol. Las selecciones como :scope + p no son válidas porque intentan seleccionar elementos que no están dentro del alcance.

@scope y especificidad

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

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

La especificidad de :scope es la de una pseudoclase 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 forma interna, el & se vuelve a escribir 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 hacer la coincidencia. Este proceso se conoce como anulació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) */
        
    }
}

La diferencia entre :scope y & dentro de @scope

Además de las diferencias en la forma en que 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 puedes usar solo una vez, ya que no puedes hacer coincidir una raíz de alcance dentro de una raíz de alcance.

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

Alcance sin preludio

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 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 nuevo criterio: proximidad del alcance. El paso se coloca después de la especificidad, pero antes del orden de aparición.

Visualización de la cascada de CSS.

Según las especificaciones:

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

Este nuevo paso resulta útil cuando anidas varias variaciones de un componente. Tomemos este ejemplo, que aún 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 usa aquí para determinar el ganador. Ve que .dark a se declaró por último, por lo que ganará con la regla .light a.

Con el criterio de proximidad del alcance, ahora se resuelve lo siguiente:

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

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

Debido a que ambos selectores a centrados tienen la misma especificidad, se activa el criterio de proximidad de alcance. Pondera ambos selectores según la proximidad a su raíz de alcance. Para ese tercer elemento a, solo hay un salto a la raíz de alcance .light, pero dos a la de .dark. Por lo tanto, ganará el selector a en .light.

Nota final: Aislamiento del selector, no aislamiento del estilo

Una nota importante es que @scope limita el alcance de los selectores, no ofrece aislamiento de diseño. 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 uno dentro de un alcance de donut, color seguirá heredando a los elementos secundarios dentro del agujero del donut.

@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)