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

CSS clásico en Elm: clases, hojas externas y el precio de la cadena de texto

La forma más simple de dar estilo a una aplicación Elm es también la más antigua y la que menos se parece al resto del lenguaje: escribir CSS en un archivo aparte, cargarlo desde el documento anfitrión y referirse a él desde la vista mediante nombres de clase que no son otra cosa que cadenas de texto. Funciona el primer día, no exige dependencias, aprovecha toda una industria de herramientas maduras y permite reutilizar el conocimiento que cualquier programador de interfaces ya tiene. Esta lección examina ese enfoque con la seriedad que merece, porque es el que más proyectos usan y el que la guía oficial asume por defecto: qué ofrecen exactamente `class`, `classList` y `style`, dónde vive la hoja de estilos respecto del compilador, y por qué la frontera entre ambos mundos es una frontera sin tipos en la que se pierden de golpe todas las garantías que el resto del programa disfruta. Se estudian después las disciplinas que la comunidad ha desarrollado para acotar ese daño sin abandonar el enfoque, y se enuncia con precisión cuál es el límite que ninguna de ellas puede superar.

⏱ 16 min

Hay un momento incómodo en la vida de todo programador que llega a Elm desde otro ecosistema, y ocurre casi siempre el mismo día: acaba de descubrir que el compilador no le deja escribir un programa que rompa en ejecución, que los estados imposibles se pueden hacer irrepresentables y que refactorizar es una operación mecánica y segura, y entonces necesita poner un botón en azul. Escribe un atributo de clase, pone entre comillas el nombre que eligió en su hoja de estilos, compila, y todo va bien. Escribe mal el nombre, compila, y todo sigue yendo bien: el programa arranca, la vista se dibuja, no hay ningún error en ninguna parte, y el botón simplemente no está azul. Esa asimetría no es un descuido del diseño ni una carencia que alguien vaya a corregir en la próxima versión. Es la consecuencia exacta de que el estilo, en la web, viva en un lenguaje distinto, con su propio archivo, su propia gramática y su propio momento de evaluación, y de que el único puente entre ese lenguaje y el nuestro sea una cadena de texto que ningún compilador de este mundo puede verificar. Entender bien ese punto es lo que permite decidir después, con criterio y no por moda, si conviene quedarse en él.

🎯 Al terminar esta lección sabrás
  • Manejar con precisión las tres puertas que Elm ofrece hacia el estilo: class, classList y style, y saber qué hace cada una y qué cuesta.
  • Situar la hoja de estilos externa respecto del compilador y explicar por qué queda fuera de su alcance de análisis.
  • Diagnosticar las clases de fallo que esta frontera sin tipos habilita y distinguir cuáles son detectables y cuáles no.
  • Aplicar las disciplinas de mitigación habituales y reconocer el límite estructural que ninguna de ellas elimina.

Tres puertas y ninguna de ellas es tipada

El módulo Html.Attributes ofrece esencialmente tres caminos hacia el aspecto de un nodo. El primero es class, que recibe una cadena y la deposita literalmente en el atributo correspondiente del elemento. El segundo es classList, que recibe una lista de pares de cadena y booleano y añade solo aquellas clases cuya condición sea verdadera, lo cual convierte la variación de aspecto en una función del modelo sin necesidad de concatenar textos a mano. El tercero es style, que recibe una propiedad y un valor y produce una declaración en línea sobre el propio nodo, saltándose la hoja externa por completo. Los tres tienen la misma propiedad esencial y conviene enunciarla sin rodeos: sus argumentos son String, y por tanto el sistema de tipos no distingue un nombre de clase existente de uno inventado, ni una propiedad válida de una errata.

import Html exposing (button, div, text)
import Html.Attributes exposing (class, classList, style)


verBoton : Bool -> Html msg
verBoton activo =
    button
        [ classList
            [ ( "boton", True )
            , ( "boton--activo", activo )
            , ( "boton--inactivo", not activo )
            ]
        ]
        [ text "Guardar" ]


verAviso : Html msg
verAviso =
    div
        [ class "aviso"
        , style "padding" "1rem"
        , style "border-left" "3px solid tomato"
        ]
        [ text "Revisa los datos" ]

La diferencia entre classList y la concatenación manual de cadenas merece un apunte, porque es el único punto de esta historia donde el lenguaje aporta algo. Al expresar la condición como un booleano dentro de la estructura, la lógica de variación queda escrita en Elm y no en una plantilla de texto: se puede extraer a una función, se puede probar, se puede derivar de un tipo unión con exhaustividad garantizada. Lo que sigue sin garantizarse es que las cadenas de la izquierda correspondan con algo que exista realmente en la hoja de estilos, y esa es exactamente la mitad del problema que nos ocupa.

.boton {
  padding: 0.5rem 1rem;
  border-radius: 4px;
  border: none;
  font: inherit;
}

.boton--activo {
  background: #89b4fa;
  color: #11111b;
}

.boton--inactivo {
  background: #45475a;
  color: #a6adc8;
  cursor: not-allowed;
}

La hoja externa vive donde el compilador no mira

Conviene tener presente el mapa completo del sistema para entender por qué esto no puede ser de otro modo. El compilador de Elm lee archivos con extensión propia, construye un grafo de módulos, comprueba tipos, elimina el código no alcanzable y emite un artefacto de JavaScript. La hoja de estilos no forma parte de ese grafo en ningún momento: la carga el documento anfitrión mediante un enlace en la cabecera, o la inyecta un empaquetador ajeno, y llega al navegador por un camino paralelo. Cuando el programa arranca, el motor de estilos del navegador aplica las reglas al árbol que Elm produjo, resolviendo por nombre y sin que nadie haya comprobado nunca que los dos lados hablen del mismo vocabulario.

De esa separación se derivan consecuencias muy concretas, y todas comparten una firma reconocible: el fallo no se manifiesta como un error sino como una ausencia. Renombrar una clase en el CSS no rompe ninguna compilación, simplemente deja huérfanos todos los nodos que la citaban. Borrar una regla que ya nadie usa exige buscar la cadena por todo el proyecto y confiar en que nadie la construyó por concatenación. La eliminación de código muerto que Elm practica con tanto rigor sobre las funciones no alcanza jamás a una regla de estilo, de modo que las hojas crecen monótonamente y nadie se atreve a podarlas. Y la cascada, que es global por diseño, permite que una regla escrita en otro archivo por otra persona altere el aspecto de un componente sin que exista ninguna relación visible entre ambos.

⚠️
El fallo silencioso es peor que el fallo ruidoso

Merece la pena detenerse en la naturaleza del error, no solo en su frecuencia. Un fallo que detiene la compilación cuesta segundos y se corrige en el sitio. Un fallo que se manifiesta como una excepción en ejecución cuesta minutos y deja rastro en un registro. Un fallo que se manifiesta como un margen que no se aplicó cuesta horas, no deja rastro alguno y, lo más grave, a menudo llega a producción porque nadie miró esa pantalla concreta en ese estado concreto. La escritura errónea de un nombre de clase pertenece a la tercera categoría, y la probabilidad de cometerla crece con el número de estados que la interfaz puede adoptar, que es precisamente donde Elm suele brillar.

🏷️

Convención de nombres

Metodologías como BEM imponen una gramática de nombres que hace visible el ámbito de cada regla. No añaden verificación, pero convierten las colisiones en algo improbable por construcción.

📦

Constantes en un módulo

Declarar los nombres una sola vez en un módulo de estilos y no escribirlos nunca en línea traslada las erratas al compilador. Sigue sin comprobarse que el CSS los defina.

🧰

Utilidades atómicas

Un catálogo cerrado y estable de clases pequeñas reduce la superficie donde inventar nombres y elimina casi toda la escritura de CSS a medida.

🔎

Análisis externo

Herramientas de purga cruzan la hoja con el fuente y avisan de reglas no citadas. Trabajan sobre texto, así que cualquier nombre calculado se les escapa.

Acotar el daño sin cambiar de enfoque

La comunidad no ha ignorado el problema, y ha producido un repertorio de disciplinas que conviene conocer porque son baratas y sirven de mucho. La más rentable de todas es también la más simple: no escribir nunca un nombre de clase en línea dentro de una vista, sino declararlo una sola vez en un módulo dedicado y referirse siempre a esa constante. La errata deja entonces de ser una cadena mal tecleada para convertirse en un nombre no definido, que es exactamente el tipo de error que el compilador de Elm sabe atrapar y explicar. La técnica no verifica que la regla exista en el CSS, pero elimina de golpe la clase entera de fallos por divergencia entre dos apariciones del mismo nombre.

module Estilos exposing (boton, botonActivo, tarjeta)


boton : String
boton =
    "boton"


botonActivo : String
botonActivo =
    "boton--activo"


tarjeta : String
tarjeta =
    "tarjeta"

La segunda disciplina consiste en reducir el espacio de nombres a un catálogo finito y ajeno, que es lo que en el fondo propone el enfoque de utilidades atómicas: si nadie inventa clases nuevas y todas provienen de un vocabulario publicado, la probabilidad de citar una inexistente baja mucho y la hoja deja de crecer. La tercera es procedimental: purgar periódicamente con herramientas que cruzan el texto del fuente con las reglas declaradas, aceptando que cualquier nombre construido dinámicamente quedará fuera de su alcance. Ninguna de las tres cambia el hecho fundamental, que conviene enunciar con claridad para no engañarse: entre el programa y su aspecto sigue habiendo una frontera de texto sin tipar, y todo lo que hacemos es reducir la superficie de esa frontera, no eliminarla.

flowchart LR
A[Fuente Elm] --> B[Compilador]
B --> C[Artefacto JavaScript]
D[Hoja de estilos] --> E[Anfitrion HTML]
C --> F[Arbol del documento]
E --> G[Motor de estilos]
F --> G
G --> H[Aspecto final]
style B fill:#a6e3a1,color:#11111b
style D fill:#f38ba8,color:#11111b
style G fill:#cba6f7,color:#11111b
💡
Cuándo `style` en línea es la respuesta correcta

Las declaraciones en línea tienen mala fama heredada de otros contextos, y en Elm conviene revisarla. Hay un caso donde son claramente lo mejor: cuando el valor es genuinamente una función continua del modelo, como la anchura de una barra de progreso o la posición de un elemento arrastrado. Expresar eso con clases exige generar decenas de reglas o recurrir a variables, mientras que en línea es una sola expresión legible y verificada por el compilador en su parte numérica. La regla práctica es simple: lo discreto y repetido va en clases, lo continuo y calculado va en línea.

Toda frontera entre dos lenguajes es una frontera sin garantías, y el nombre que la cruza es siempre el eslabón débil

Lo que ocurre aquí con el CSS es un caso particular de un fenómeno que reaparece en todas las escalas del oficio, y verlo así vale más que memorizar el caso. Cada vez que dos sistemas con sus propias reglas deben coordinarse, alguien tiene que elegir un vocabulario compartido, y ese vocabulario acaba materializándose en cadenas de texto: el nombre de la columna en la consulta, la clave del diccionario de configuración, la ruta en la llamada al servicio, el identificador del evento, el nombre de la clase en la hoja de estilos. La cadena es la moneda universal de la interoperabilidad precisamente porque no compromete a nada: cualquier lenguaje sabe producirla y cualquier lenguaje sabe consumirla. Ese mismo no comprometerse es lo que la vuelve el punto ciego permanente de cualquier sistema de tipos, por bueno que sea, porque un tipo solo puede hablar de lo que su compilador ve, y por definición el otro lado de la frontera es lo que no ve. De ahí se sigue una consecuencia estratégica que excede con mucho a esta lección. La calidad de un sistema no la determina el rigor de su zona más verificada sino la cantidad y el grosor de sus costuras, y toda decisión de arquitectura es, mirada desde este ángulo, una decisión sobre dónde poner las costuras y cuántas admitir. Las respuestas posibles son solo tres, y todas aparecerán en este nivel: mover la frontera hacia dentro para que el compilador la vea, que es lo que hace generar el CSS desde el propio lenguaje; estrechar la frontera hasta un catálogo mínimo y estable, que es lo que hacen las utilidades atómicas; o eliminarla sustituyendo el otro lenguaje por una biblioteca del propio, que es lo que hace elm-ui. Ninguna es gratis, todas tienen un coste que se paga en otro sitio, y elegir bien exige haber entendido primero que el problema nunca fue el CSS: era la costura.

⚔️ Mide la costura de tu proyecto
  1. Toma una vista real y cuenta cuántas cadenas de texto distintas cruzan hacia la hoja de estilos; anota cuántas aparecen más de una vez escritas a mano.
  2. Introduce deliberadamente una errata en un nombre de clase, compila y describe con precisión qué te dice el sistema y en qué momento te enteras.
  3. Extrae todos los nombres a un módulo de constantes y repite el experimento; razona qué clase de fallo has eliminado y cuál sigue intacta.
  4. Sustituye una variación booleana hecha por concatenación de cadenas por un classList y explica qué gana la lógica al vivir en Elm.
  5. Localiza en tu hoja una regla que ya no cite nadie y estima cuánto tiempo llevaba ahí; propón un procedimiento para que eso no vuelva a pasar.
  6. Identifica un valor de aspecto que sea función continua del modelo y decide si pertenece a una clase o a una declaración en línea, justificando el criterio.
  7. Escribe la defensa más honesta que puedas del enfoque clásico frente a las alternativas, enumerando lo que ganarías y lo que perderías al abandonarlo.