Diferencias reales con Sass: lo que no se puede hacer
Por qué la concatenación de nombres con & no funciona en CSS nativo y no funcionará nunca, qué más se queda fuera, y en qué se diferencian las construcciones que existen en los dos.
La confusión número uno al adoptar el anidamiento nativo tiene nombre y apellidos: &__elemento. Funciona en Sass desde hace más de una década, es la base de cómo casi todo el mundo escribe BEM, y en CSS nativo no funciona ni va a funcionar. No es una carencia pendiente de especificar: es la consecuencia directa de una decisión de diseño sin la cual el anidamiento nativo no podría existir tal como existe.
- Explicar por qué
&no puede concatenar nombres y qué lo impide técnicamente. - Enumerar el resto de lo que Sass hace y CSS nativo no.
- Predecir en qué difiere el resultado de construcciones que existen en ambos.
- Decidir con criterio qué parte de la cadena de compilación puedes retirar.
Lo que no se puede hacer: concatenar nombres
En Sass, & es una cadena de texto que se sustituye antes de emitir el archivo. Como es texto, se puede pegar a otro texto:
// Sass. Emite .card, .card__titulo y .card--destacada.
.card {
padding: 1rem;
&__titulo { font-weight: 600; }
&--destacada { border-color: gold; }
}
En CSS nativo, & es un selector. No es texto y no se puede pegar a nada. &__titulo no produce .card__titulo: no produce nada, porque es un selector inválido y la regla se descarta.
/* CSS nativo. Esto NO existe. La regla se tira entera. */
.card {
padding: 1rem;
&__titulo { font-weight: 600; }
}
Lo mismo vale para cualquier variante: &--modificador, &-suffix, &.#{$nombre}. Todo lo que dependa de tratar el selector padre como una cadena está fuera.
Y no es un hueco que se vaya a rellenar más adelante. Si & fuera texto, el selector final de una regla anidada no podría conocerse sin expandir toda la cadena de ancestros, y entonces el navegador no podría representar la regla anidada como una regla de verdad en el CSSOM, ni mostrarla en las herramientas de desarrollo con su selector resuelto, ni asociar un elemento con la regla que lo estila sin rehacer la expansión. La referencia semántica es lo que hace el sistema analizable; la concatenación de texto es incompatible con ello.
Tienes tres salidas, y conviene entender qué cuesta cada una.
Escribir los nombres completos. La más directa y la que recomiendo. Pierdes la brevedad y ganas que el nombre exista literalmente en el archivo.
.card { padding: 1rem; }
.card__titulo { font-weight: 600; }
.card--destacada { border-color: gold; }
Reestructurar a descendientes. Cambia la semántica: ahora .titulo solo se estila dentro de .card, y la especificidad sube de (0,1,0) a (0,2,0). Puede ser lo que quieres, pero es una decisión, no una traducción.
.card {
padding: 1rem;
& .titulo { font-weight: 600; }
&.destacada { border-color: gold; }
}
Quedarte con el preprocesador solo para esa parte. Es una opción legítima si tu base de código tiene diez mil selectores BEM, pero conviene saber que estás manteniendo una cadena de compilación por una única característica.
El patrón &__elemento tiene un defecto que se lleva denunciando desde que se popularizó y que casi nadie considera decisivo hasta que le toca mantener un proyecto ajeno: el nombre de la clase no existe en ningún archivo del repositorio. Inspeccionas un elemento en el navegador, ves class="card__titulo", buscas card__titulo en el código y no aparece nada, porque en el origen pone &__titulo dentro de un bloque que puede estar a doscientas líneas de distancia, en otro archivo importado, y con el prefijo compuesto a su vez desde otro nivel de anidamiento. Has roto la propiedad más valiosa que tiene una base de código: que el identificador que ves sea buscable. Todo lo demás se degrada en cadena a partir de ahí. Renombrar deja de ser una sustitución de texto y pasa a exigir entender la estructura. Un análisis de clases muertas deja de ser posible sin ejecutar el compilador. Un editor no puede ofrecerte “ir a la definición” sobre una clase. Un desarrollador nuevo no puede orientarse. Que el anidamiento nativo prohíba la concatenación no es una limitación que haya que tolerar: es la restricción que te obliga a escribir nombres completos, y con ellos recuperas el buscador, el renombrado seguro, el análisis estático y la posibilidad de leer el CSS sin ejecutar nada. La incomodidad de escribir .card__titulo entero es real y dura una semana; el beneficio de que ese nombre exista dura todo el proyecto. Cuando migres, resiste la tentación de reproducir la comodidad con una capa de herramientas: la comodidad era el problema.
Lo demás que Sass hace y CSS no
| Sass | CSS nativo | Sustituto realista |
|---|---|---|
&__x, &--y |
no existe | nombres completos |
@mixin / @include |
no disponible aún | custom properties, capas, utilidades |
@extend |
no existe | listas de selectores, capas |
@each, @for, @while |
no existe | generar en el build o escribirlo |
@function |
especificado, implementación parcial | calc(), color-mix(), colores relativos |
mapas y map.get |
no existe | custom properties con nombres |
$variable |
distinto: --var vive en la cascada |
custom properties |
propiedades anidadas font: { } |
no existe | escribir las longhand |
@at-root |
no existe por diseño | sacar la regla del bloque |
@use / @forward |
no existe | módulos del empaquetador, @layer |
Merecen comentario tres filas.
@extend no tiene sustituto y es una buena noticia. Su problema nunca fue la ausencia en CSS sino su semántica: mueve el selector que extiende hasta la posición del selector extendido en el archivo de salida, lo cual cambia el orden en la cascada de formas que no se deducen leyendo el código. Sustituirlo por una lista de selectores o por una capa es siempre más predecible.
Los mixins están en camino pero no puedes contar con ellos. Hay una especificación de funciones y mixins en CSS, y ya existe implementación parcial en un motor. Hasta que esté en los cuatro, cualquier proyecto que dependa de mixins necesita o bien el preprocesador o bien reformular el patrón con custom properties, que suele ser posible y suele quedar mejor.
$variable y --variable no son lo mismo y no se sustituyen mecánicamente. Una vive en la compilación y desaparece; la otra vive en la cascada, se hereda y se puede cambiar en tiempo de ejecución. Es el tema del nivel siguiente, y la migración de una a otra no es un renombrado: es un cambio de modelo.
Lo que existe en ambos pero no significa lo mismo
El anidamiento existe en los dos, y ahí es donde aparecen las diferencias silenciosas: el código compila, no da error, y hace algo distinto.
El & con una lista de padres. Ya lo has visto con números en la lección anterior, pero conviene verlo lado a lado, porque es la divergencia con más consecuencias.
// Sass emite DOS reglas independientes, cada una con su peso.
.a, #b { & .c { color: red; } }
// -> .a .c (0,2,0)
// -> #b .c (1,1,0)
/* CSS nativo emite UNA regla con el peso del mas especifico. */
.a, #b { & .c { color: red; } }
/* -> :is(.a, #b) .c (1,1,0) para todos los casos */
Un elemento que encaja por .a recibe la declaración con peso (0,2,0) en Sass y con peso (1,1,0) en CSS nativo. Si en tu hoja había otra regla intermedia, el resultado visual cambia al migrar sin que ninguna línea del bloque se haya tocado.
El orden de las declaraciones respecto a las reglas anidadas. Sass recoge todas las declaraciones del bloque y las emite juntas en la regla del padre; CSS nativo las deja donde las escribiste. En el caso normal el resultado coincide. Deja de coincidir en cuanto declaras la misma propiedad dos veces alrededor de una regla anidada, que es justo el escenario donde alguien está intentando algo raro y no quiere sorpresas. La regla de higiene ya la conoces: declaraciones primero, reglas anidadas después.
La profundidad no cuesta lo mismo. En Sass, cada nivel de anidamiento se expande a texto y el coste es de tamaño de archivo. En CSS nativo, cada nivel añade un :is() que el motor evalúa, y aunque el coste por evaluación es pequeño, existe. No es motivo para no anidar; sí lo es para no anidar seis niveles porque el editor lo permite.
Qué queda de Sass en 2026
La pregunta honesta no es “puedo quitar Sass” sino “qué me lo retiene”. Haz la lista y casi siempre queda corta.
Si lo que te retiene es el anidamiento, ya no te retiene nada: el nativo lo cubre. Si son las variables, tampoco, salvo que dependas de que se resuelvan en compilación para producir valores distintos en archivos distintos. Si es el color, las funciones nativas —color-mix(), los colores relativos con from, OKLCH— hacen más y mejor que las de Sass, porque operan en espacios perceptuales y se resuelven en tiempo de ejecución. Si son los archivos parciales y el orden de importación, la combinación de módulos del empaquetador con @layer lo resuelve con más control.
Lo que de verdad retiene a un proyecto suele reducirse a dos cosas: bucles que generan familias de utilidades y mapas de configuración. Ambos son problemas de generación de código, no de CSS, y se resuelven igual de bien con un script de veinte líneas que emita el archivo, sin arrastrar un lenguaje entero.
Y si migras, hazlo en este orden: primero el anidamiento, que es mecánico; después los nombres concatenados, que es la parte tediosa y la que más beneficio deja; después las variables, que exige pensar en herencia; y al final los bucles, que probablemente decidas dejar donde están.
- Coge un bloque BEM con
&__y reescríbelo con nombres completos. Cuenta cuántas búsquedas fallaban antes. - Traduce a CSS nativo un bloque con
.a, #bcomo padre y comprueba el cambio de especificidad. - Sustituye un
@extendpor una lista de selectores y explica qué cambia en el orden. - Reemplaza una función de color de Sass por
color-mix()y compara el resultado en OKLCH. - Haz el inventario de qué te retiene el preprocesador en un proyecto real y decide qué queda fuera.