wandres.dev
ARQUITECTURA I · De BEM a los tokens de diseño

El problema que todas las metodologías intentaban resolver

Qué tenía exactamente el CSS de 2010 que hacía imposible escalar una hoja de estilos, y por qué la respuesta tuvo que ser disciplina humana en lugar de una función del lenguaje.

⏱ 17 min

Es fácil leer BEM o ITCSS hoy y pensar que sus autores se complicaban la vida. No lo hacían: resolvían con convenciones de nombres un problema que el lenguaje no tenía forma de resolver, porque las herramientas que hoy das por hechas —capas, ámbito, especificidad cero, variables que viven en la cascada— no existían, ni estaban propuestas, ni había consenso de que hicieran falta. Entender el problema original es lo único que permite juzgar qué parte de esa disciplina sigue teniendo sentido y qué parte es ritual heredado.

🎯 Al terminar esta lección sabrás
  • Enumerar las cuatro propiedades del CSS clásico que hacían inevitable el desorden.
  • Explicar por qué la especificidad produce una escalada y no un equilibrio.
  • Reconocer la forma que toma una hoja de estilos que ha perdido el control.
  • Situar cronológicamente las herramientas que faltaban en cada momento.

Cuatro propiedades y una consecuencia

El CSS clásico tenía cuatro características que, por separado, son razonables, y que juntas producían un sistema imposible de escalar.

Un único espacio de nombres global. Todo selector que escribes compite con todo selector que exista en la página, venga de donde venga. No hay módulos, no hay importación con renombrado, no hay privado. .title de tu componente y .title de la biblioteca de terceros que integraste el mes pasado son la misma cosa.

Alcance no acotado. Un selector descendente como .sidebar a alcanza a cualquier enlace que aparezca en cualquier profundidad dentro de un .sidebar, presente o futuro. En el momento de escribirlo eso parece una ventaja: escribes poco y cubres mucho. Seis meses después, cuando alguien mete un componente nuevo en la barra lateral, tu regla lo alcanza sin que nadie lo pretendiera y nadie sabe de dónde viene el subrayado.

Especificidad acumulativa e irreversible. Cuando dos reglas compiten, gana la más específica. Y como la única herramienta que tienes para ganar es añadir especificidad, la única jugada disponible en cada conflicto es escribir un selector más específico que el anterior. Eso no converge: escala. La partida termina siempre en el mismo sitio, y el sitio se llama !important.

Sin recolección de basura. Nada te dice qué reglas ya no se usan. Un selector sigue en el fichero para siempre porque nadie puede demostrar que borrarlo es seguro. La hoja crece de forma monótona, y a partir de cierto tamaño el coste de leerla supera el coste de añadirle algo, con lo que todo el mundo añade y nadie borra.

La consecuencia de las cuatro juntas tiene una firma reconocible. Un fichero de miles de líneas donde las últimas doscientas son parches. Selectores de siete niveles. Un !important cada treinta líneas. Reglas duplicadas con valores casi iguales. Y el síntoma definitivo: nadie del equipo se atreve a borrar nada, así que la hoja solo crece.

ℹ️
El experimento mental que lo deja claro

Imagina un lenguaje de programación donde todas las variables son globales, cualquier función puede modificar cualquier variable, el orden de resolución de conflictos depende de cuántos puntos tenga el nombre de la variable, y no hay forma de saber si una variable se usa todavía. Nadie escribiría un programa de cien mil líneas con eso. El CSS de 2010 era exactamente eso, y la industria estaba escribiendo aplicaciones enteras con él.

Por qué la especificidad escala

Merece la pena detenerse en el tercer punto porque es el motor de casi todo el daño y es el peor entendido.

Imagina dos desarrolladores trabajando en la misma hoja. El primero escribe .boton { background: azul }. El segundo necesita que el botón de la barra sea rojo y escribe .barra .boton { background: rojo }. Perfectamente razonable. El primero necesita ahora que el botón deshabilitado sea gris incluso en la barra, y escribe .barra .boton.deshabilitado. El segundo necesita una excepción en la barra de la página de checkout, y escribe #checkout .barra .boton. Y así.

Cada paso es local y sensato. Ninguno es un error. Pero el sistema completo tiene una propiedad terrible: la especificidad solo puede subir. No existe forma de decir “esta regla debe perder”, porque no hay especificidad negativa. La única acción disponible en un conflicto es incrementar, y como cada incremento hace más difícil el siguiente conflicto para todos los demás, el equilibrio del sistema es un solo punto: el máximo. Un #id .clase .clase que nadie puede vencer sin recurrir a !important, momento en el que empieza otra escalada idéntica un nivel más arriba.

Esto no es un fallo de disciplina. Es un fallo de diseño del lenguaje que solo se pudo arreglar cuando llegaron dos cosas: :where(), que permite escribir un selector con especificidad cero —o sea, una regla que pierde a propósito—, y @layer, que separa el orden de la especificidad y permite decir “esta capa entera pierde contra aquella, digan lo que digan los selectores”.

El orden y la fragilidad

Hay un quinto factor que se menciona menos y causa la mitad de los bugs de integración: cuando la especificidad empata, gana el que viene después en el orden del documento. El orden depende del orden de los @import, del orden en que el empaquetador concatena ficheros, y —en cuanto hay carga diferida— del orden en que llegan las respuestas de red.

Es decir: en una aplicación con división de código, el resultado visual podía depender de qué fragmento de CSS terminaba de descargarse antes. Eso convertía un bug de estilo en un bug de concurrencia, con toda la diversión que eso implica: no reproducible en local, aparece solo con red lenta, desaparece al abrir las herramientas de desarrollo.

Todas las metodologías clásicas dedican una parte importante de sus reglas a hacer que el orden no importe, precisamente porque no había forma de controlarlo. BEM lo consigue haciendo que dos reglas casi nunca compitan. ITCSS lo consigue imponiendo un orden global explícito y prohibiendo saltárselo. Son dos estrategias distintas para el mismo agujero.

Cronología de lo que faltaba

Situar las fechas cambia por completo la lectura de cada metodología, porque hace evidente que ninguna podía usar lo que no existía.

Año Qué había Qué faltaba
2009 selectores, especificidad, @import todo lo demás
2011 preprocesadores con variables de compilación variables en la cascada
2013 Flexbox empezando ámbito, capas, :where()
2016 custom properties en la mayoría de motores capas, ámbito, :is y :where
2019 grid, custom properties consolidadas capas, ámbito, container queries
2022 :is, :where, @layer llegando ámbito, container queries
2023 container queries, :has, anidamiento @scope
2026 todo lo anterior más @scope

Fíjate en la última columna de 2009: todo lo demás. Nicole Sullivan escribió OOCSS con selectores y nada más. Harry Roberts escribió ITCSS sin capas en cascada. El autor de BEM no tenía @scope. Cuando leas sus reglas, la pregunta correcta no es “¿esto sigue siendo buena idea?” sino “¿qué mecanismo del lenguaje falta aquí, y ya existe?”. Esa pregunta reorganiza toda la disciplina clásica, y es la que recorre las cuatro lecciones siguientes.

La disciplina que sustituye a una función del lenguaje siempre es más cara y siempre es peor

Hay una lección general que conviene extraer antes de mirar ninguna metodología concreta, porque se aplica muchísimo más allá del CSS. Cuando un sistema no ofrece un mecanismo que la gente necesita, la gente lo construye con convenciones. Las convenciones funcionan —BEM funcionaba de verdad, no era teatro— pero tienen tres costes que un mecanismo real no tiene. Se pagan en cada uso: el mecanismo lo aplica el motor una vez; la convención la aplica una persona cada vez que escribe una línea, y una persona distinta cada vez que la revisa. No se pueden verificar: el compilador no te dice que has violado BEM, solo te lo dice un compañero en la revisión, si se fija. Y degradan bajo presión: la primera vez que alguien tiene una urgencia a las siete de la tarde, la convención se rompe, y a partir de ahí la hoja tiene dos sistemas conviviendo. Por eso el patrón histórico se repite siempre igual: una convención popular es un informe de bug contra el lenguaje, y el lenguaje acaba absorbiéndola. BEM pedía ámbito y llegó @scope. ITCSS pedía capas y llegó @layer. Los prefijos de utilidad pedían especificidad controlable y llegó :where(). La forma madura de leer cualquier metodología, en CSS o fuera, es preguntarse qué carencia denuncia; y la forma madura de evaluar tu propio código es fijarte en qué reglas repites en cada revisión de código, porque ahí está el mecanismo que te falta.