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

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

Navegadores compatibles

  • 118
  • 118
  • x
  • x

El delicado arte de escribir selectores CSS

Cuando escribes selectores, es posible que te encuentres dividido entre dos mundos. Por un lado, debes ser bastante específico sobre los elementos que seleccionas. Por otro lado, te conviene que los selectores sean fáciles de anular y no estén estrechamente vinculados a la estructura del DOM.

Por ejemplo, si deseas seleccionar "la imagen de héroe en el área de contenido del componente de la tarjeta", que es una selección de elementos bastante específica, lo más probable es 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 el código.
  • Al confiar en el combinador secundario directo, está estrechamente vinculado a la estructura del DOM. Si el lenguaje de marcado cambia, también debes cambiar tu CSS.

Sin embargo, tampoco es recomendable que escribas solo img como selector para ese elemento, ya que esto seleccionaría todos los elementos de imagen de tu página.

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

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

Pero ¿qué pasa si no necesitas nada de eso? ¿Qué pasaría si CSS te ofreciera 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? Aquí es donde entra en juego @scope, que te ofrece una manera de seleccionar elementos solo dentro de un subárbol de tu DOM.

Presentación de @scope

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

Por ejemplo, para apuntar solo a los elementos <img> en el componente .card, configura .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 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 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 estilo con alcance solo se orienta a elementos <img> que están ubicados entre elementos .card y .card__content en el árbol principal. Este tipo de alcance, con un límite inferior y superior, a menudo se denomina alcance de anillo

Selector de :scope

De forma predeterminada, todas las reglas de estilo con alcance son relativas a la raíz de alcance. También es posible orientarse 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 alcance se anteponen de manera implícita :scope. Si quieres, puedes ser explícito al respecto. Para ello, debes anteponer :scope por tu cuenta. Como alternativa, puedes anteponer el selector & desde Anidació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 en sí 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 de @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 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, internamente, & se vuelve a escribir en el selector que se usa para la raíz de permisos, 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 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 siguiendo las reglas de especificidad de :is(): la especificidad de & es la de su argumento más específico.

Aplicada a 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). Si lo combinas con la especificidad de img, que es (0,0,1), 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 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 coincidir con 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 otra.

@scope (.card) {
  & & { /* Selects a `.card` in the matched root .card */
  }
  :root :root { /* ❌ 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 principal contenedor 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 elementos dentro de div con el nombre de clase card__header, ya que ese 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 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 la especificación:

Cuando se comparan las declaraciones que aparecen en las 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 es útil para anidar diferentes variaciones de un componente. Toma 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 pequeño 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 del orden de aparición que la cascada utiliza aquí para determinar el 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 se resuelve:

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

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

Debido a que ambos selectores a con alcance tienen la misma especificidad, se implementa el criterio de proximidad de alcance. Pesa 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 hasta el uno de .dark. Por lo tanto, ganará el selector a en .light.

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

Es importante tener en cuenta que @scope limita el alcance de los selectores, ya que no ofrece aislamiento de estilo. Las propiedades que se hereden de forma secundaria también lo harán, más allá del límite inferior de @scope. Una de esas propiedades es la de color. Cuando se declara que uno dentro del alcance de la rosquilla, el 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)