La regla @scope

Estilos CSS con ambito del DOM limitado


Históricamente, uno de los principales problemas de CSS para un programador, es que CSS es de naturaleza global. Esto significa que en todo momento debemos tener en cuenta los estilos de todo el documento, cosa que en aplicaciones grandes es muy complejo. Cuando desarrollamos ejemplos pequeños en CSS, no suelen haber demasiados problemas. Sin embargo, cuando el código CSS o el proyecto crece, comienzan los problemas.

Pero eso ha cambiado con la regla @scope.

La regla @scope

La regla @scope nos permite aplicar estilos a un ámbito limitado, es decir, reducir el HTML al que se le aplican los estilos. Esto es una herramienta genial, muy interesante para evitar conflictos CSS y simplificar nuestro código.

La sintaxis puede ser cualquiera de las siguientes:

ReglaDescripción
@scopeLimita el alcance al contenedor donde se declara. Usado en un <style> del HTML.
@scope (start)Limita el alcance inicial a partir del cuál se aplican los estilos.
@scope (start) to (end)Delimita el alcance inicial y final del cuál se aplican los estilos.

Mediante estas tres modalidades de uso de @scope podemos limitar el alcance de prácticamente todas las formas. Veamos cada una de ellas en detalle.

La regla @scope en línea

La forma más simple de aplicar la regla @scope es escribirla directamente en el HTML, definiéndola mediante una etiqueta <style>. Observa el siguiente ejemplo. La regla @scope definida en una etiqueta <style> actúa sólo en el ámbito de su etiqueta padre, o sea, el <div class="container">, por lo que los estilos están limitados sólo a la etiqueta <strong> interior:

<p>Hola, soy <strong>ManzDev</strong> exterior.</p>

<div class="container">
  <style>
    @scope {
      strong {
        background: orangered;
        color: white;
        padding: 0.2rem;
      }
    }
  </style>
  <p>Hola, soy el <strong>ManzDev</strong> interior.</p>
</div>

Repasemos varios detalles clave aquí:

  • El primer <strong> está fuera de .container.
  • El segundo <strong> está dentro de .container.
  • El @scope se declara en un <style> dentro de .container, por lo que sólo le afecta a él.

Esto es la funcionalidad básica de @scope. Sin embargo, no es obligatorio utilizarlo en el HTML en una etiqueta <style>, se puede utilizar en hojas de estilo CSS externas con otra sintaxis, como veremos a continuación.

La regla @scope en CSS

La regla @scope también se puede utilizar desde ficheros .css externos, pero su sintaxis cambia un poco. Ahora delimitaremos el alcance de las etiquetas HTML a las que afectará, indicando un principio y un final en los parámetros de la regla @scope. De esta forma, indicamos a que partes del documento queremos que afecten los estilos:

Regla @scope CSS

En esta imagen se puede ver que el alcance de los estilos comienza en la etiqueta .grandparent (excluyéndola) hasta la etiqueta .child (excluyéndola), por lo que afecta sólo a .parent.

Límite inicial de @scope

Veamos un fragmento de código utilizando la regla @scope sobre el código HTML de la imagen anterior. Entre paréntesis delimitaremos el ámbito inicial para el que se van a aplicar los estilos CSS. Atentos a los detalles:

  • 1️⃣ El @scope comienza desde el interior de .grandparent (este excluído)
  • 2️⃣ El CSS añade un borde verde a todos los <div>
  • 3️⃣ El HTML define 3 etiquetas <div>, una dentro de otra, con un texto cada una.
@scope (.grandparent) {
  div {
    border: 2px solid green;
    padding: 3px;
  }
}
<div class="grandparent">
  Grandparent
  <div class="parent">
    Parent
    <div class="child">
      Child
    </div>
  </div>
</div>

Observa que en la demo, los estilos se están aplicando a los elementos .parent y .child. Esto ocurre porque le hemos dicho a la regla @scope que comience a aplicar estilos a todos los <div> a partir del elemento .grandparent (excluído).

Ten en cuenta que en estos ejemplos estamos seleccionando elementos genéricos div para que sea más sencillo el ejemplo, pero se puede complicar con cualquier otro selector, haciéndolo más potente.

Límite final con to

Ahora, vamos a añadir la palabra clave to y un segundo parámetro a la regla @scope, que nos permitirá no sólo indicar el límite inicial, sino también el límite final. Nuevamente, atentos a los detalles:

  • 1️⃣ El @scope comienza desde el interior de .grandparent (este excluído)
  • 2️⃣ El @scope termina en el elemento .child (este excluído)
@scope (.grandparent) to (.child) {
  div {
    border: 2px solid green;
    padding: 3px;
  }
}
<div class="grandparent">
  Grandparent
  <div class="parent">
    Parent
    <div class="child">
      Child
    </div>
  </div>
</div>

Observa que sólo se está aplicando el borde al elemento .parent. A .child puede parecer que se le aplica, pero observa que no tiene borde como en el ejemplo anterior, simplemente aparece dentro porque son elementos anidados.

Por ejemplo, si modificamos el límite final e indicamos .child > * (todo lo que incluya el .child), entonces si que detectará el último elemento .child:

@scope (.grandparent) to (.child > *) {
  div {
    border: 2px solid green;
    padding: 3px;
  }
}
<div class="grandparent">
  Grandparent
  <div class="parent">
    Parent
    <div class="child">
      Child
    </div>
  </div>
</div>

O observa el siguiente ejemplo, donde aplicamos los límites desde body hasta .child (este excluído). Te encontrarás con que los dos primeros elementos .grandparent y .parent tienen borde, mientras que .child no lo tiene:

@scope (body) to (.child) {
  div {
    border: 2px solid green;
    padding: 3px;
  }
}
<div class="grandparent">
  Grandparent
  <div class="parent">
    Parent
    <div class="child">
      Child
    </div>
  </div>
</div>

Límite final inexistente

Podría darse la circunstancia de que definamos un límite de ámbito final que realmente no existe en el HTML, como por ejemplo la etiqueta .grandson:

@scope (.grandparent) to (.grandson) {
  div {
    border: 2px solid green;
    padding: 3px;
  }
}
<div class="grandparent">
  Grandparent
  <div class="parent">
    Parent
    <div class="child">
      Child
    </div>
  </div>
</div>

Como el elemento con clase .grandson no existe, el navegador interpretará la regla scope como si no tuviera límite final, es decir, como @scope (.grandparent). De esta forma, estaría seleccionando todos los <div> que se encuentren en el interior de un .grandparent (este excluído).

Múltiples límites

Otro detalle interesante sobre la regla @scope es que podemos utilizar una lista de múltiples valores, de forma similar al combinador lógico :is() o :where(). Observa el siguiente ejemplo:

@scope (.post-container, .comments-container) to (.items) {
  .element {
    background: indigo;
    color: white;
  }
}
<div class="post-container">
  <div class="element">Element 1 (Selected)</div>
  <div class="items">
    <div class="element">Element 2 (Not selected)</div>
    <div class="element">Element 3 (Not selected)</div>
  </div>
</div>

<div class="element">Element 1 (Not Selected)</div>

<div class="comments-container">
  <div class="element">Element 1 (Selected)</div>
  <div class="items">
    <div class="element">Element 2 (Not selected)</div>
    <div class="element">Element 3 (Not selected)</div>
  </div>
</div>

En este ejemplo, dentro de los selectores de los paréntesis indicamos múltiples valores, por lo que podemos establecer selecciones múltiples y no atarnos a elecciones más sencillas. Esto puede resultar especialmente útil de cara a la mantenibilidad.

La pseudoclase :scope

La pseudoclase :scope se puede utilizar en las reglas @scope para hacer referencia al ámbito inicial del scope. Por ejemplo, en el siguiente fragmento de código, :scope es una referencia a .grandparent:

@scope (.grandparent) {
  :scope > div {
    border: 2px solid orangered;
    padding: 5px;
    margin: 10px;
  }
}
<div class="grandparent">
  Grandparent
  <div class="parent">
    Parent
    <div class="child">
      Child
    </div>
  </div>
</div>

En este caso, el navegador aplicaría los estilos sólo a .parent, ya que es el <div> que está inmediatamente a continuación de .grandparent, que es el :scope. Esta característica podría ser realmente útil si utilizamos una lista de múltiples valores:

@scope (.grandparent, .container) {
  :scope > .element {
    border: 2px solid orangered;
    padding: 5px;
    margin: 10px;
  }
}

Aquí, estaríamos seleccionando todos los elementos .element que son hijos inmediatamente directos de .grandparent o de .container, que son los dos ámbitos de :scope en este ejemplo.

También podríamos seleccionar elementos según sus ancestros:

@scope (.grandparent) to (.child) {
  .dark :scope > .element {
    background: #222;
    color: white;
    padding: 15px;
  }
}

En este caso, seleccionamos los .element que estén dentro de un .grandparent que tenga algún padre con la clase .dark. Podríamos también utilizar el carácter & del Nesting CSS en lugar del :scope, ya que funciona de una forma muy similar.

Reemplazo de BEM + Sass

Una característica muy interesante que brinda @scope es la posibilidad de crear bloques de «componentes CSS» al estilo de como se suele hacer con Sass, pero de forma nativa. Observemos el siguiente código Sass, donde aprovechamos nomenclatura BEM para crear nombres de clase que eviten especificidades altas o nombres de clase potencialmente colisionables.

En Sass lo haríamos así:

.post-container {
  &__title { font-size: 3rem; }
  &__subtitle { font-size: 1.5rem; }
}

.content-container {
  &__code { color: #848848; }
}
<div class="post-container">
  <h3 class="post-container__title">Título</h3>
  <p class="post-container__subtitle">Subtítulo</p>
</div>

<div class="content-container">
  <p>Utilizando la regla <code class="content-container__code">@scope</code> en CSS.</p>
</div>

Esto, al pasarlo por un procesador de Sass, permitiría producir un código CSS donde los nombres de clases anidados que empiecen por __, se concatenan (atención al nombre de clases). De esta forma el selector tiene una especificidad mínima, aunque sea más complejo:

.post-container__title { font-size: 3rem; }
.post-container__subtitle { font-size: 1.5rem; }
.content-container__code { color: #848848; }

Durante mucho tiempo, esto ha sido una de las formas de evitar aumentar la especificidad de CSS en proyectos que utilizan Sass, a la vez que facilitan una forma predecible de nombrar elementos.

Ahora, con @scope, en su lugar podríamos aprovechar la regla @scope (incluso usando los límites superiores si queremos excluir alguna parte) para obtener un código nativo muy fácil de leer, modificar, mantener y delimitar:

@scope (.post-container) {
  .title { font-size: 3rem; }
  .subtitle { font-size: 1.5rem; }
}

@scope (.content-container) to (.items) {
  .code { color: #848848; }
}
<div class="post-container">
  <h3 class="title">Título</h3>
  <p class="subtitle">Subtítulo</p>
</div>

<div class="content-container">
  <p>Utilizando la regla <code class="code">@scope</code> en CSS.</p>
</div>

¿Quién soy yo?

Soy Manz, vivo en Tenerife (España) y soy streamer partner en Twitch y profesor. Me apasiona el universo de la programación web, el diseño y desarrollo web y la tecnología en general. Aunque soy full-stack, mi pasión es el front-end, la terminal y crear cosas divertidas y locas.

Puedes encontrar más sobre mi en Manz.dev