wandres.dev
ESTILOS Y UI · elm-ui y CSS

Por qué existe elm-ui: herencia sorpresa, colapso de márgenes y especificidad

Una biblioteca que propone abandonar el estándar universal de la presentación web necesita una justificación mucho más sólida que el gusto de su autor, y `elm-ui` la tiene: nace de un diagnóstico preciso sobre tres familias de fallo que el CSS no puede evitar porque están inscritas en su propio diseño. Esta lección examina las tres con detalle técnico. La primera es la herencia implícita, que hace que un elemento reciba propiedades que nadie declaró en él y que dependen de dónde acabó colocado. La segunda es el modelo de caja y su fenómeno más desconcertante, el colapso de márgenes, donde dos separaciones declaradas producen una sola de tamaño impredecible. La tercera es la cascada con su algoritmo de especificidad, que hace que la regla vencedora se decida por un cómputo que ninguna de las partes implicadas puede consultar localmente. Sobre cada una se analiza qué solución adopta `elm-ui`, qué propiedad gana el sistema al adoptarla y qué capacidad concreta se pierde a cambio, para que la valoración final no sea un acto de fe sino una comparación informada.

⏱ 17 min

Casi todo el mundo que ha escrito CSS profesionalmente comparte una experiencia difícil de admitir en voz alta: la de haber resuelto un problema de disposición sin llegar a entender del todo por qué la solución funcionaba. Se prueba una cosa, no va; se prueba otra, tampoco; se añade un contenedor más, y de pronto encaja. Uno sigue adelante porque el trabajo no espera, pero queda la sensación incómoda de haber negociado con el sistema en vez de haberlo instruido. Esa sensación no es una carencia personal ni un síntoma de haber estudiado poco: es la consecuencia previsible de trabajar con un sistema donde el resultado visible de un elemento no está determinado por lo que se escribió sobre ese elemento, sino por la interacción de tres mecanismos globales —la herencia, el modelo de caja y la cascada— que fueron diseñados por separado, en momentos distintos, para resolver el problema de dar formato a documentos de texto. elm-ui no nace de la idea de que el CSS esté mal hecho, sino de la observación de que resuelve un problema distinto del que tenemos, y de la sospecha de que sus tres mecanismos centrales, retirados a la vez, dejan un hueco mucho menor de lo que todo el mundo teme.

🎯 Al terminar esta lección sabrás
  • Explicar la herencia implícita como fuente de acoplamiento a distancia y localizar sus manifestaciones típicas.
  • Describir el modelo de caja y el colapso de márgenes con precisión suficiente para predecir el resultado en los casos límite.
  • Reconstruir el algoritmo de especificidad y razonar por qué convierte la resolución de conflictos en un cómputo no local.
  • Contrastar, para cada uno de los tres, la solución que adopta elm-ui y la capacidad concreta que se sacrifica.

Propiedades que llegan sin que nadie las escribiera

La herencia del CSS establece que ciertas propiedades declaradas en un elemento se transmiten a sus descendientes salvo que estos las anulen. La motivación histórica es impecable: en un documento de texto es razonable que el tipo de letra fijado en el cuerpo alcance a los párrafos sin repetirlo mil veces. El problema aparece cuando ese mecanismo se aplica a interfaces compuestas por piezas escritas de forma independiente, porque entonces el aspecto de un fragmento depende del lugar del árbol donde acabe insertado, y ese lugar no es una propiedad del fragmento sino de quien lo usa. Un componente correcto en un contexto se rompe en otro sin que nadie haya tocado su código, y el diagnóstico exige recorrer la cadena de ancestros hasta encontrar al responsable.

El conjunto de propiedades heredables tampoco es intuitivo, y ahí está la segunda mitad del problema. La tipografía se hereda, el color del texto se hereda, la alineación horizontal del texto se hereda; el fondo no, el borde no, el relleno no. La lista es histórica y hay que aprenderla de memoria porque no responde a ninguna regla general que se pueda deducir. La consecuencia es que ni siquiera un programador experto puede afirmar, mirando un elemento aislado, qué propiedades tiene activas: puede afirmar cuáles declaró, que no es lo mismo.

/* Escrito por alguien, hace dos anos, en otro archivo */
.panel-lateral {
  font-size: 0.85rem;
  text-align: center;
  color: #6c7086;
}

/* Un componente correcto que se rompe solo por estar dentro */
.tarjeta__titulo {
  font-weight: 600;
}

En elm-ui la herencia se restringe a la tipografía y al color del texto, que son los casos donde resulta genuinamente útil, y se declara de forma explícita en el punto donde se aplica. Todo lo demás es estrictamente local: un elemento tiene el relleno, el fondo, el borde y el tamaño que dicen sus atributos, y ningún ancestro puede alterarlos. La propiedad que se gana es la que hace posible razonar sobre piezas aisladas: una función que devuelve un Element msg produce el mismo resultado visual independientemente de dónde se la invoque, lo que convierte la reutilización en algo seguro en lugar de algo que hay que probar caso por caso.

import Element exposing (column, el, padding, spacing, text)
import Element.Font as Font


-- La tipografia se hereda, pero esta escrita donde empieza a valer
verPanel : List String -> Element msg
verPanel lineas =
    column
        [ Font.size 14
        , Font.color (rgb255 108 112 134)
        , spacing 8
        , padding 12
        ]
        (List.map text lineas)


-- Este titulo no puede recibir nada mas del panel que lo contenga
verTitulo : String -> Element msg
verTitulo t =
    el [ Font.size 20, Font.bold ] (text t)
⚠️
El acoplamiento a distancia no aparece en ninguna revisión de código

Lo que hace peligrosa a la herencia implícita no es que exista, sino que es invisible en el momento en que se introduce el fallo. Quien añade una propiedad tipográfica a un contenedor genérico no está mirando los cuarenta descendientes que pasarán a heredarla, y quien escribe uno de esos descendientes tres meses después no tiene manera de saber qué le va a llegar desde arriba. Ninguna revisión de código detecta este acoplamiento, porque los dos cambios implicados son individualmente razonables y viven en archivos distintos. Solo lo detecta el ojo de alguien que mire la pantalla concreta en el estado concreto donde se manifiesta.

Cajas que suman mal y márgenes que desaparecen

El segundo mecanismo es el modelo de caja, y su primer efecto sorprendente es que el tamaño declarado de un elemento no coincide con el espacio que ocupa, porque relleno y borde se suman por fuera salvo que se cambie el modelo de cálculo. Ese ajuste se hace hoy de forma casi universal en la primera línea de cualquier hoja, lo cual es un dato revelador: una corrección global aplicada por todo el mundo es la confesión de que el valor por defecto era el equivocado. Pero el fenómeno realmente desconcertante es otro, y no admite corrección: el colapso de márgenes verticales.

La regla dice que dos márgenes verticales adyacentes no se suman, sino que se funden en uno solo cuyo tamaño es el mayor de los dos. Adyacentes significa aquí más cosas de las que parece: el margen inferior de un hermano con el superior del siguiente, pero también el margen superior de un hijo con el de su padre cuando entre ambos no hay borde ni relleno, en cuyo caso el margen del hijo se escapa fuera del padre y separa al padre de lo que tenga encima. Ese último caso es el que produce el clásico hueco que aparece donde nadie lo puso y que se corrige añadiendo un relleno de un píxel, un borde transparente o un contexto de formato nuevo, tres remedios que funcionan por razones que no tienen ninguna relación con el problema.

.contenedor {
  background: #1e1e2e;
}

.contenedor .titulo {
  margin-top: 24px; /* Este margen se escapa fuera del contenedor */
}

/* Los tres remedios habituales, ninguno expresa la intencion */
.contenedor--a { padding-top: 1px; }
.contenedor--b { border-top: 1px solid transparent; }
.contenedor--c { display: flow-root; }

La solución de elm-ui es eliminar el margen del vocabulario. La separación entre hermanos se declara una vez en el contenedor con spacing, y como es el contenedor quien la declara, no hay dos declaraciones que fundir ni ninguna que pueda escaparse hacia fuera. El relleno interior se declara también en el contenedor con padding y afecta al hueco entre su borde y su contenido. Con esas dos operaciones se cubre lo que en CSS requiere margen, relleno, colapso, contextos de formato y los selectores de primer y último hijo que todo el mundo escribe para quitar el hueco sobrante de los extremos. Lo que se pierde es la capacidad de que un hijo concreto tenga una separación distinta de la de sus hermanos, que se resuelve envolviéndolo o partiendo el contenedor.

🧬

Herencia

En CSS llega desde cualquier ancestro y la lista de propiedades es histórica. En elm-ui solo la tipografía y el color, declarados donde se aplican.

📏

Modelo de caja

En CSS el tamaño declarado no es el ocupado y los márgenes verticales colapsan. En elm-ui no hay margen y el tamaño incluye el relleno.

⚖️

Especificidad

En CSS gana la regla con más peso calculado, no la más cercana. En elm-ui no hay reglas ni selectores, solo atributos del elemento.

🎯

Localidad

El resultado de las tres decisiones es el mismo: lo que le pasa a un elemento está escrito en el elemento o en su padre inmediato.

Una regla gana y no es la que escribiste

El tercer mecanismo es la cascada. Cuando varias reglas declaran la misma propiedad para el mismo elemento, el ganador se decide por un algoritmo que compara primero el origen y la importancia, después la especificidad —un número compuesto que cuenta identificadores, clases y etiquetas del selector— y solo en caso de empate el orden de aparición. El algoritmo es determinista y está bien documentado, así que el problema no es la imprevisibilidad teórica sino algo más incómodo: para saber qué regla gana hay que conocer todas las reglas que compiten, y esas reglas viven en archivos que no se están mirando, escritos por personas que no están presentes, cargados en un orden que depende de la configuración del empaquetador.

De ahí nace la patología más conocida del oficio, que es la escalada de especificidad. Una regla no se aplica, se le añade un selector padre para que pese más, funciona; alguien más adelante necesita anularla y añade dos; al cabo de un tiempo aparece la declaración de importancia forzada, que gana a todo y que a su vez genera una segunda escalada dentro de su propio nivel. Nada de esto es ignorancia: es la respuesta racional de cada individuo a un sistema donde la única manera de tener razón es pesar más que el resto.

.boton { background: #89b4fa; }
.panel .boton { background: #a6e3a1; }
#cabecera .panel .boton { background: #f9e2af; }
.tema-oscuro .boton { background: #45475a !important; }

Merece la pena separar dos críticas que suelen confundirse, porque solo una de ellas es sólida. La primera dice que la cascada es impredecible, y es falsa: el algoritmo es público, determinista y cualquiera puede ejecutarlo a mano. La segunda dice que la cascada no es local, y es la que importa: para aplicar un algoritmo determinista hacen falta todas sus entradas, y en este caso las entradas son el conjunto completo de reglas del proyecto más su orden de carga. Un sistema puede ser perfectamente determinista y aun así impedir el razonamiento, si para determinar cualquier cosa hay que conocerlo entero. Esa distinción es la que explica por qué la experiencia de escribir CSS no mejora indefinidamente con la experiencia del programador.

elm-ui no tiene selectores, y por tanto no tiene cascada ni especificidad. Los atributos se aplican al elemento en el que están escritos, y cuando dos atributos de la lista entran en conflicto gana el último, que es la regla más simple posible y además la única que se puede verificar mirando una sola línea. Lo que se sacrifica es real y conviene nombrarlo: desaparecen los temas globales por conmutación de una clase en la raíz, desaparece la posibilidad de que un tercero ajuste el aspecto de tu aplicación desde fuera, y desaparecen los selectores relacionales que dependen del estado de un hermano. Los dos primeros se resuelven pasando el tema como argumento a las funciones de vista, que es más verboso pero explícito; el tercero exige reformular el problema en términos de estado del modelo.

flowchart TD
A[Problema del CSS] --> B[Herencia implicita]
A --> C[Colapso de margenes]
A --> D[Especificidad y cascada]
B --> E[Solo tipografia y color explicitos]
C --> F[Sin margen: spacing en el padre]
D --> G[Sin selectores: gana el ultimo atributo]
E --> H[Resultado local y predecible]
F --> H
G --> H
style A fill:#f38ba8,color:#11111b
style H fill:#a6e3a1,color:#11111b
Los tres mecanismos no son defectos: son la respuesta correcta a un problema que ya no tenemos

Sería cómodo cerrar esta lección concluyendo que el CSS está mal diseñado, y sería falso. Herencia, cascada y modelo de caja son soluciones excelentes al problema para el que se concibieron, que era dar formato a documentos de texto enlazados, escritos por autores que no se conocen entre sí, en un medio donde el destinatario tiene tanto derecho a opinar sobre la presentación como el emisor. En ese contexto, la herencia evita repetir la tipografía en cada párrafo; la cascada permite que las preferencias del usuario, la hoja del autor y los valores por defecto del navegador convivan sin que ninguno tenga que conocer a los otros; el margen que colapsa produce una separación tipográfica uniforme entre bloques sin que nadie la calcule. Cada uno de los tres mecanismos es una forma de acoplamiento débil deliberado, y el acoplamiento débil es exactamente lo que se necesita cuando las partes no pueden coordinarse. El giro que explica todo lo demás es que una aplicación no es un documento. Sus partes las escribe un mismo equipo, se compilan juntas, se despliegan juntas y se conocen entre sí perfectamente. En ese contexto el acoplamiento débil deja de ser una virtud y pasa a ser lo que impide razonar, porque el precio que se paga por permitir que cualquiera influya sobre cualquiera es que nadie puede afirmar nada mirando una sola pieza. La lección general es una de las más útiles del oficio y casi nunca se enuncia: heredamos herramientas junto con los supuestos que las justificaban, y esos supuestos rara vez viajan escritos. Cuando una tecnología produce dificultades persistentes que ningún experto acaba de eliminar, la pregunta productiva no es cómo dominarla mejor, sino qué problema estaba resolviendo cuando se diseñó y si ese sigue siendo el problema que tenemos delante.

⚔️ Documenta tus propias cicatrices
  1. Busca en tu proyecto tres reglas que existan solo para anular otra regla y reconstruye la cadena que llevó hasta ellas.
  2. Localiza un componente cuyo aspecto cambie según dónde se inserte y determina qué propiedad heredada lo provoca y desde qué ancestro.
  3. Reproduce el colapso de márgenes entre padre e hijo, explica por qué ocurre y prueba los tres remedios habituales; razona por qué ninguno nombra el problema.
  4. Calcula a mano la especificidad de cuatro reglas en conflicto de tu hoja y predice cuál gana; comprueba tu predicción en el navegador.
  5. Cuenta cuántas declaraciones de importancia forzada hay en tu proyecto y clasifica cada una según qué la hizo necesaria.
  6. Reescribe una de esas zonas conflictivas con row, column y atributos locales, y anota qué expresividad has perdido realmente.
  7. Elige el mecanismo del CSS que más te cueste abandonar y construye el argumento más fuerte a favor de conservarlo.