wandres.dev
ÁMBITO DE ESTILOS · @scope, raíz y límite

El problema del ámbito y la sintaxis de @scope

Por qué CSS es global por diseño, qué costaba cada intento anterior de acotarlo, cómo se escribe @scope y por qué acota reglas y no elementos.

⏱ 17 min

CSS nació global y sigue siéndolo: cualquier regla de cualquier archivo puede encajar con cualquier elemento del documento. Durante veinticinco años, la única defensa fue la disciplina de nombres, y todas las alternativas serias —el atributo que se retiró, el shadow DOM, el renombrado en compilación— resolvían el problema cobrando un precio en otra parte. @scope es el primer mecanismo que acota el alcance de una regla sin salirse del lenguaje, y entender qué acota exactamente es lo que evita la decepción.

🎯 Al terminar esta lección sabrás
  • Explicar por qué la globalidad de CSS es una decisión de diseño y no un defecto.
  • Comparar el coste de las cuatro soluciones anteriores al ámbito.
  • Escribir las tres formas del preludio de @scope.
  • Distinguir acotar reglas de encapsular elementos.

Por qué CSS es global

La globalidad no fue un descuido. CSS se diseñó para separar la presentación del documento, y la unidad de presentación era la página entera: una hoja definía cómo se ven los h1 del sitio, no los de un componente. Que una regla alcance a todo el documento era el objetivo, no un efecto secundario.

El modelo empezó a rozar cuando la unidad de trabajo dejó de ser la página y pasó a ser el componente. Un componente quiere ser una unidad sellada: sus reglas dentro, las de fuera fuera. CSS no tenía ninguna forma de expresarlo, así que la industria improvisó cuatro, y cada una compró el aislamiento con una moneda distinta.

Los intentos anteriores

El atributo scoped en <style>. La solución obvia, especificada en su día y retirada porque las implementaciones eran complicadas y la demanda se canalizó hacia el shadow DOM. Su idea —el bloque de estilos afecta al subárbol donde vive— ha vuelto, y ahora es un caso particular de @scope.

Convenciones de nombres. BEM y sus primos no acotan nada: garantizan que no habrá colisiones si todo el mundo cumple. Cuestan disciplina permanente, nombres largos y ninguna garantía real, porque una sola clase escrita a mano en un archivo antiguo rompe el invariante.

Renombrado en compilación. CSS Modules, styled-components, scoped de Vue. Funciona de verdad, y cuesta una cadena de compilación, nombres ilegibles en el inspector y una capa de indirección entre lo que escribes y lo que se ejecuta.

Shadow DOM. El único aislamiento auténtico de la plataforma: los selectores de fuera no entran y los de dentro no salen. Cuesta caro: hay que construir el componente como elemento personalizado, la tematización global deja de funcionar salvo por herencia y custom properties, y muchas cosas que dabas por hechas —selectores que atraviesan, :has() desde fuera, hojas globales— dejan de estar disponibles.

La sintaxis de @scope

@scope toma un preludio con una raíz y opcionalmente un límite, y un bloque de reglas que solo aplican dentro de ese ámbito.

@scope (.card) {
  img { border-radius: 0.5rem; }
  h3 { font-weight: 600; margin-block: 0; }
  a { color: currentColor; }
}

Esos tres selectores solo encajan dentro de un .card. Fuera de un .card, ninguna img, ningún h3 y ningún a reciben nada. La raíz está incluida en el ámbito: una regla que encaje con .card en sí misma también aplica.

Hay tres formas del preludio:

/* Raiz sola. */
@scope (.card) { }

/* Raiz y limite: el donut, que veras en la leccion siguiente. */
@scope (.card) to (.contenido-externo) { }

/* Sin preludio: solo dentro de un elemento style en el HTML. */

La tercera es la que recupera la vieja idea del atributo retirado. Un <style> colocado dentro del subárbol puede abrir un @scope sin preludio, y la raíz es el elemento padre de ese <style>:

<article class="tarjeta">
  <style>
    @scope {
      img { border-radius: 0.5rem; }
      :scope { padding: 1rem; }
    }
  </style>
  <img src="/foto.avif" alt="">
  <p>Texto de la tarjeta.</p>
</article>

Esto es especialmente útil en plantillas de servidor y en frameworks que emiten HTML por fragmentos: el estilo viaja con el marcado y no puede escaparse, sin herramientas y sin renombrar nada.

@scope también se puede anidar dentro de una regla de estilo, y entonces la raíz se resuelve con las reglas de anidamiento que ya conoces:

.card {
  @scope (&) to (.slot) {
    p { margin-block: 0.5rem; }
  }
}

Lo que @scope no hace

Aquí está el malentendido que hay que desactivar antes de seguir. @scope acota las reglas que tú escribes, no protege a los elementos de las reglas de los demás.

flowchart LR
subgraph S[Con scope]
  A[Mis reglas] -- No salen --> B[Resto del documento]
  C[Reglas de fuera] -- Si entran --> D[Mi subarbol]
end
subgraph SD[Con shadow DOM]
  E[Mis reglas] -- No salen --> F[Resto del documento]
  G[Reglas de fuera] -- No entran --> H[Mi arbol sombra]
end
style A fill:#a6e3a1,color:#11111b
style C fill:#f9e2af,color:#11111b
style G fill:#a6e3a1,color:#11111b

Un p { color: red } global sigue aplicando dentro de tu @scope, y ganará o perderá por las reglas normales de la cascada. @scope es unidireccional: contiene lo que emites, no filtra lo que recibes.

Tampoco corta la herencia. Un color heredado desde body entra en el subárbol como siempre, y eso es deseable: quieres que tus componentes hereden la tipografía y el color del tema.

Y no crea ningún tipo de aislamiento de layout ni de contexto de formato. @scope es exclusivamente un mecanismo de selección y de cascada.

Mecanismo Contiene lo que emites Filtra lo que recibes Coste
Convención de nombres por disciplina no vigilancia constante
Renombrado en build no cadena de compilación
@scope no ninguno, es CSS
Shadow DOM modelo de componente completo
💡
La combinación que casi nadie usa

@scope y shadow DOM no compiten: se complementan. Dentro de un shadow root, donde el aislamiento ya está resuelto hacia fuera, @scope sigue siendo útil para separar las partes internas de un componente complejo sin inventar nombres. Y en una página sin componentes web, @scope te da el ochenta por ciento del beneficio con el cero por ciento del coste arquitectónico.

Acotar reglas y encapsular elementos son problemas distintos

La decepción típica con @scope viene de esperar que haga lo que hace el shadow DOM, y la raíz del malentendido es que llamamos “ámbito” a dos cosas opuestas. Acotar reglas es una propiedad del autor: yo declaro dónde puede aplicar lo que escribo, y con eso protejo al resto del documento de mis efectos. Encapsular elementos es una propiedad del componente: nadie de fuera puede alcanzar mis nodos, y con eso me protejo yo de los efectos de los demás. La primera resuelve el problema de quien escribe una librería y no quiere romper la página de nadie; la segunda resuelve el de quien integra código ajeno y no quiere que le rompan el suyo. Son necesidades reales y distintas, y la plataforma tiene una herramienta para cada una precisamente porque no se pueden fusionar sin perder algo: una encapsulación bidireccional obliga a inventar puertas —::part(), ::slotted(), custom properties heredadas— para dejar pasar la tematización, y esas puertas son la mitad de la complejidad del shadow DOM. @scope renuncia a la protección entrante y a cambio no necesita puertas: tu componente sigue heredando el tema, sigue respondiendo a las utilidades globales y sigue siendo inspeccionable. La pregunta correcta al elegir no es cuál aísla más, sino de quién te tienes que proteger. Si el riesgo es que tus reglas se escapen, @scope basta y es gratis. Si el riesgo es que entren reglas que no controlas —un CMS, un widget de terceros, una hoja heredada de la que nadie responde—, ninguna cantidad de @scope te va a salvar y necesitas shadow DOM.

⚔️ Sitúa el ámbito
  1. Escribe un @scope que estile las imágenes de una tarjeta sin afectar a las de fuera.
  2. Comprueba que una regla global sigue entrando dentro de tu ámbito y explica por qué.
  3. Monta el caso del <style> sin preludio dentro de un fragmento de HTML.
  4. Clasifica los cuatro mecanismos de la tabla según el problema que resuelven, no según cuánto aíslan.
  5. Coge un componente de tu proyecto y decide si necesita @scope, shadow DOM o ninguno.