12k
All articles

Alcance nativo en CSS con @scope

@scope nativo de CSS limita los selectores a un componente o límite donut, con reglas de cascada por proximidad y sin build.

OpenReplay Team
OpenReplay Team
Alcance nativo en CSS con @scope

@scope es una regla at-rule de CSS que restringe dónde puede coincidir un bloque de selectores, desde un elemento raíz hasta un límite inferior opcional, sin necesidad de un paso de compilación y sin añadir especificidad. MDN le otorga el estado Baseline “newly available” (recientemente disponible) con fecha de marzo de 2026.

Si mantienes un código base de componentes, ya estás pagando el precio de la contención de estilos en algún lugar: una convención de nomenclatura que impones en las revisiones de código, un plugin de bundler que aplica hashes a los nombres de clase, o un runtime que inyecta etiquetas de estilo. Cada uno de ellos existe porque un selector descendiente simple llega demasiado lejos, y un selector de hijo encadenado suelda el CSS a una forma exacta del DOM.

Puntos clave

  • Dentro de un bloque @scope, un selector simple conserva únicamente su propia especificidad, porque el prefijo implícito es :where(:scope) y :where() aporta peso cero; escribir :scope explícitamente añade 0-1-0.
  • @scope (.card) to (.card__content) coincide con los elementos situados entre la tarjeta y su slot de contenido: la raíz se incluye, y el elemento límite junto con todo lo que hay debajo quedan excluidos.
  • Cuando dos declaraciones con alcance empatan en especificidad, gana aquella cuya raíz de alcance esté a menos saltos del DOM respecto al elemento, sin importar el orden en el código fuente.
  • La proximidad de alcance se compara después de la importancia, las capas de cascada y la especificidad, y antes del orden en el código fuente, de modo que un selector sin alcance con mayor especificidad sigue prevaleciendo sobre una regla con alcance.
  • @scope limita dónde coinciden los selectores; no impide que las propiedades heredadas, como color, atraviesen el límite del alcance hacia la región excluida.

¿Por qué se filtran los selectores y qué hacen BEM, CSS Modules y CSS-in-JS al respecto?

Cada selector CSS se evalúa contra el documento completo, por lo que apuntar a “la imagen hero de esta tarjeta” obliga a elegir entre un selector demasiado estructural y uno demasiado amplio.

.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 */

BEM resuelve el problema del alcance excesivo mediante disciplina de nomenclatura: un nombre de bloque único, y cada elemento interno lleva una clase block__element, de modo que nunca existe un .title a secas. CSS Modules automatiza la misma idea reescribiendo cada nombre de clase como un identificador hasheado y local al archivo en tiempo de compilación. Las bibliotecas CSS-in-JS lo hacen en tiempo de ejecución o de compilación, generando esos identificadores a partir del código de tus componentes; el estado actual de ese ecosistema se trata por separado. El borrador de CSS Cascade Level 6 detalla cómo funcionan esas herramientas internamente: etiquetan cada elemento de un componente con un atributo o clase marcadora, y luego añaden ese marcador a cada selector del archivo.

EnfoquePaso de compilaciónLímite inferior (donut)Proximidad en la cascadaEspecificidad extra
BEMNoSolo por convención de nomenclaturaNoNivel de clase por nombre
CSS ModulesClases hasheadas por archivoNoNivel de clase por nombre
CSS-in-JSRuntime o compilaciónClase generada por componenteNoNivel de clase por nombre
@scopeNoCláusula nativa to (limit)Ninguna desde la raíz

El bloque básico de alcance CSS: @scope (.card)

@scope (.card) { img { } } coincide únicamente con los elementos <img> que son descendientes inclusivos de un .card, y la raíz .card no aporta nada a la especificidad 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 */
}

Las notas de MDN sobre la especificidad dentro de un alcance explican el mecanismo. Un selector simple dentro del bloque se evalúa como si tuviera el prefijo :where(:scope) delante, y dado que :where() no aporta peso propio, la raíz no suma nada al total. Esto es lo contrario de lo que hacen las herramientas que estampan atributos: un gancho generado como [data-v-abc123] añade peso a nivel de atributo a cada selector que toca. Escrito explícitamente, :scope es una pseudoclase normal y añade 0-1-0, de modo que :scope img es 0-1-1.

¿Qué es un alcance de tipo donut?

Un alcance donut, escrito @scope (.card) to (.card__content), aplica estilos a todo desde la tarjeta hacia abajo, pero sin incluir .card__content ni su subárbol, que es exactamente el problema del contenido insertado (slotted) que generan los componentes anidados.

<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 defecto, la propia raíz cuenta como dentro del alcance y el elemento límite no. Añadir > * a cualquiera de los dos selectores invierte ese límite. @scope (.card) to (.card__content > *) incorpora el propio elemento .card__content al alcance sin dejar de excluir a sus hijos, lo cual resulta útil cuando el contenedor del slot necesita padding pero el contenido insertado debe quedar intacto. Según la definición del borrador, un elemento califica cuando se sitúa en la raíz o por debajo de ella, y no es un límite ni está por debajo de uno. Ninguna combinación de selectores expresa “descendiente de X pero no dentro de Y” sin recurrir a una clase de frontera en cada elemento o a cadenas de :not() que reintroducen especificidad.

La proximidad de alcance prevalece sobre el orden en el código fuente

Cuando dos reglas con alcance empatan en especificidad, gana la declaración cuya raíz de alcance esté más cerca del elemento, lo que corrige el error de los temas anidados que los selectores descendientes simples resuelven mal.

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

Con selectores ordinarios, el párrafo más interno coincide tanto con .theme-light p como con .theme-dark p a 0-1-1, por lo que gana la regla que aparezca en último lugar en la hoja de estilos, y el párrafo se renderiza con el color del tema oscuro a pesar de estar dentro de un contenedor claro.

@scope (.theme-light) {
  p { color: #1b1b1b; }
}
@scope (.theme-dark) {
  p { color: #f2f2f2; }   /* declared later, but loses on the inner p */
}

El <p> más interno está a un salto de su raíz .theme-light y a dos de .theme-dark, por lo que se aplica la regla clara. MDN recorre este mismo ejemplo práctico, y bajo la regla de proximidad de la especificación una regla sin raíz de alcance nunca puede ganar esta contienda, porque su número de saltos se considera infinito.

¿Dónde se sitúa la proximidad en la cascada?

La proximidad de alcance se compara después de la importancia, las capas de cascada y la especificidad, y antes del orden en el código fuente, de modo que un selector sin alcance con mayor especificidad sigue prevaleciendo sobre una regla con alcance, por muy cerca que esté la raíz del alcance.

El orden de clasificación de Cascade 6 enumera siete criterios en orden de precedencia descendente:

  1. Origen e importancia
  2. Contexto (encapsulación del shadow tree)
  3. El atributo style
  4. Capas de cascada
  5. Especificidad
  6. Proximidad de alcance
  7. Orden de aparición

La proximidad es un criterio de desempate para la especificidad, no un sustituto de ella:

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

Situar la proximidad por debajo de la especificidad fue una decisión deliberada, y el registro de cambios del borrador documenta la eliminación de la versión más fuerte que la habría colocado por encima. El razonamiento aparece en el explicador de la característica: si la proximidad ganara primero, la especificidad solo resolvería contiendas entre selectores con la misma proximidad, de modo que las reglas escritas para situarse por encima o por debajo unas de otras empezarían a ganar o perder según la forma del DOM. Si esperas que los estilos con alcance se comporten como la encapsulación del Shadow DOM, este es el punto donde esa expectativa falla.

Los selectores tienen alcance; la herencia no

@scope limita dónde puede coincidir un selector; no impide que las propiedades heredadas atraviesen el límite del alcance, de modo que un color establecido en la raíz del alcance sigue llegando a todos los elementos dentro de la región excluida.

<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 */
}

El párrafo insertado queda fuera del alcance, por lo que ningún selector con alcance coincide con él y no recibe borde. Aun así se renderiza en hotpink, porque la herencia es un mecanismo a nivel de propiedad que se ejecuta después de la cascada y desconoce por completo los alcances. La referencia de @scope señala lo mismo: el alcance delimita a qué elementos puede llegar un selector, no dónde terminan los estilos resultantes. Todo lo que quieras contener en la frontera del slot necesita un reinicio explícito en el propio slot, igual que siempre.

¿Qué navegadores admiten @scope y cuándo deberías usarlo?

MDN sitúa @scope en Baseline “newly available” desde marzo de 2026, lo que significa que todos los motores principales actuales lo incluyen. Los datos de compatibilidad muestran el primer soporte en Chrome y Edge 118 y Firefox 146; Safari 17.4 lo incorporó, Safari 26.0 a 26.3 están marcados como parciales, y Safari 26.4 restauró el soporte completo. Los navegadores que carecen de él descartan la regla at-rule completa, como exige CSS para las construcciones no reconocidas, de modo que un bloque con alcance degrada a nada en lugar de a una regla rota.

Recurre a @scope cuando un componente sea dueño de una región del árbol pero no deba aplicar estilos a lo que se inserta en él, o cuando el mismo componente se anide dentro de sí mismo con variantes distintas. Déjalo de lado para resets globales, tipografía y tokens de marca: esos deben fluir libremente, y la cascada está construida para que así sea. Cuando necesites ordenar hojas de estilo completas unas respecto a otras en lugar de delimitar subárboles, las capas de cascada siguen siendo la herramienta adecuada.

@scope traslada la contención de tu pipeline de compilación al navegador: las fronteras de tipo donut y la resolución por proximidad son ahora características de la cascada, no convenciones ni hashes generados. El siguiente paso práctico es tomar un componente que actualmente dependa de un sufijo __element o de una clase hasheada para mantener intacto su slot interno, reescribirlo como un bloque @scope (.component) to (.slot), y comprobar que cualquier color o fuente que esperabas que se detuviera en el slot se reinicia allí de forma explícita.

Preguntas frecuentes

¿Puedo detectar la compatibilidad de @scope con @supports, y qué ocurre en los navegadores que carecen de él?

Los navegadores sin soporte para @scope descartan el bloque de la at-rule, por lo que las reglas con alcance no se aplican y nada más se rompe. CSS Conditional Rules Level 5 define @supports at-rule(@scope), pero at-rule() es más reciente que @scope (Chromium 148 fue el primero en incorporarlo), así que cualquier navegador lo bastante antiguo como para no tener @scope tampoco dispone de la función de detección. Escribe reglas de respaldo simples con especificidad igual o menor fuera del bloque; los navegadores compatibles las sobrescribirán con las reglas con alcance.

¿Puedo usar @scope sin un selector raíz?

Sí. Dentro de un elemento style de HTML puedes escribir @scope sin selector raíz en su preludio, y el navegador aplicará el alcance de las reglas contenidas al elemento padre de ese elemento style. La forma inline también admite un límite, escrito como @scope to (.card__content). Esto encaja bien con fragmentos renderizados en el servidor que envían sus propios estilos. En hojas de estilo ordinarias, usa la forma de preludio (root) para que la raíz del alcance quede explícita.

¿Puedo leer las reglas @scope desde JavaScript?

Sí, a través de la interfaz CSSScopeRule, que extiende CSSGroupingRule. Expone dos propiedades de cadena de solo lectura: start devuelve el selector serializado de la raíz del alcance y end devuelve el selector del límite del alcance, cada uno con valor null cuando esa parte del preludio se omite. Accede a una regla mediante document.styleSheets y su lista cssRules, y a las reglas de estilo con alcance que contiene a través de la propiedad cssRules heredada de CSSGroupingRule.

¿Cuál es la diferencia entre @scope y Shadow DOM para la encapsulación de estilos?

Shadow DOM crea un árbol DOM separado con una frontera de estilos rígida: los selectores externos no pueden coincidir dentro de un shadow root salvo mediante ::part, y las reglas internas no pueden salir hacia fuera. @scope no cambia nada del marcado; solo limita dónde pueden coincidir los selectores dentro de un bloque, de modo que otras hojas de estilo siguen pudiendo apuntar a cualquier elemento del alcance. Las propiedades heredadas atraviesan ambas fronteras. Además, @scope no necesita JavaScript ni un shadow root.

Open-source session replay

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

We use cookies to improve your experience. By using our site, you accept cookies.