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.
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.
- 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 | sí | no | cadena de compilación |
@scope |
sí | no | ninguno, es CSS |
| Shadow DOM | sí | sí | modelo de componente completo |
@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.
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.
- Escribe un
@scopeque estile las imágenes de una tarjeta sin afectar a las de fuera. - Comprueba que una regla global sigue entrando dentro de tu ámbito y explica por qué.
- Monta el caso del
<style>sin preludio dentro de un fragmento de HTML. - Clasifica los cuatro mecanismos de la tabla según el problema que resuelven, no según cuánto aíslan.
- Coge un componente de tu proyecto y decide si necesita
@scope, shadow DOM o ninguno.