Dónde acaba el framework y dónde empiezo yo

Dos volúmenes de hormigón vistos desde abajo contra un cielo azul muy oscuro. El de delante, blanco y al sol, enseña su esquina en el centro de la imagen; detrás asoma otro en penumbra gris. No se ve ninguna ventana ni ningún otro detalle del edificio.
Gustavo LeightonUnsplash License

Un framework de CSS te regala la estética pero te la cobra en control. La pregunta no es quién manda en la tipografía o en los colores, que es como suele plantearse: es dónde se escribe el cambio. Mientras cada cosa se pueda cambiar en un sitio y solo uno, el reparto funciona. El día que para mover algo haya que tocar el framework, el reparto se rompió.

Es la segunda parte de una serie sobre cómo está hecho este sitio: la primera contaba cómo se publica y esta cuenta cómo se ve.

El layout salió del contenido, no al revés

Antes de elegir nada miré qué tenía que enseñar este sitio: un menú y una lista de artículos. Punto. No hay panel de usuario, ni carrito, ni un buscador global, ni secciones que se abran en cascada.

Con ese inventario, un layout complicado no resuelve ningún problema: se lo inventa. Así que elegí el más simple que hacía el trabajo — una columna con el menú a un lado y el contenido al otro — y me pareció además el más cómodo de leer, que para un sitio que es casi todo texto no es un detalle menor.

Lo que hay hoy son tres estados, no dos:

  • Móvil: una sola columna. Todo el armazón del sitio —identidad, menú, selector de idioma y selector de tema— vive en una barra fija abajo, y el contenido se queda con la pantalla entera.
  • A partir de 768px: aparece la rejilla de dos columnas. A la izquierda, la identidad del sitio con el menú debajo; a la derecha, el contenido; el pie cruzando por abajo. La barra inferior desaparece.
  • A partir de 1280px: la identidad sube a una fila propia a todo el ancho y deja la columna izquierda entera para el menú.

Los dos cortes no son números elegidos a ojo: son los breakpoints md y xl que Pico ya usa. Inventarme los míos habría significado tener dos rejillas de tamaños que no coinciden.

Mobile-first no es empezar por el móvil

El nombre engaña un poco. Mobile-first no describe en qué orden diseñas, sino qué CSS se aplica cuando no se cumple ninguna condición. En este sitio, el estado base —todo lo que está escrito fuera de cualquier media query— es el móvil, y las pantallas grandes se construyen añadiendo encima con min-width.

Lo hice así por dos motivos:

El primero es qué se ve cuando las media queries no llegan a aplicarse. El estado base es el CSS que no tiene ninguna condición, así que es lo único que le queda a un navegador que no las evalúa. Si ahí está descrito un escritorio de dos columnas, en una pantalla de 360px salen dos columnas apretadas; si está descrito el móvil, sale una columna, que cabe en cualquier pantalla. El fallo cae del lado bueno.

El segundo es que se escribe menos y se piensa mejor. Con min-width cada bloque añade: la rejilla, la barra lateral, el header a todo el ancho. Con max-width cada bloque desdice lo anterior, y acabas con reglas cuyo único trabajo es apagar otra regla de más arriba.

Hay un detalle que aprendí por las malas y que está anotado en el propio CSS: mezclar max-width: 767px con min-width: 768px deja un hueco. Los anchos de pantalla no son números enteros —un zoom o un móvil con densidad rara te da 767,5px— y en ese medio píxel no se cumple ninguna de las dos condiciones. Por eso el sitio no mezcla los dos sentidos: o todo suma hacia arriba, o no hay frontera fiable.

El menú va abajo

En móvil, la barra del sitio está pegada al borde inferior de la pantalla. No es lo que hace casi ninguna web, y quiero explicar por qué creo que casi ninguna web tiene razón.

El argumento es que el pulgar no llega arriba. Y no es una impresión mía: en 2013 Steven Hoober observó a más de 1.300 personas usando el móvil por la calle y publicó cómo lo sujetan de verdad. Cerca de la mitad lo llevaban con una sola mano, y los pulgares se llevaban tres de cada cuatro interacciones con la pantalla. De ahí sale el mapa que se conoce como thumb zone: la franja de abajo se alcanza sin esfuerzo, los laterales a media altura piden estirar, y las esquinas de arriba obligan a recolocar el teléfono o a usar la otra mano. Los móviles no han hecho más que crecer desde entonces, así que la esquina superior izquierda —donde tanta web pone el menú— está hoy más lejos que cuando se midió.

Lo llamativo es que las aplicaciones sí lo hacen. La barra inferior es el patrón estándar tanto en Material Design como en las tab bars de iOS. Quien diseña aplicaciones lleva años poniendo la navegación abajo; quien diseña webs la sigue poniendo arriba.

Buscando el porqué me encontré con que no es solo inercia del escritorio. Hay una razón técnica de verdad: en una web, el borde inferior no es tuyo, es del navegador. Safari en iOS puso su barra de direcciones justo ahí, y esa barra aparece y desaparece según haces scroll, así que una barra fija abajo se pelea con ella. Existe solución —env(safe-area-inset-bottom), que le pregunta al navegador cuánto espacio se está quedando— pero es un problema que en una aplicación nativa sencillamente no tienes.

Es decir: puse el menú abajo por comodidad y tenía razón en la comodidad, pero también me metí en el único sitio de la pantalla que la web no controla del todo.

Por qué Pico

Pico es un framework de CSS mínimo y sin clases: no te pide escribir class="btn btn-primary btn-lg" por todas partes, sino que estiliza los elementos de HTML directamente. Escribes un <button> y ya está decente.

Lo elegí por eso y porque es ligero. Me daba hechas dos cosas que no me apetecía resolver desde cero —una estética coherente y el modo claro/oscuro— sin obligarme a aprender un vocabulario de clases que solo sirve dentro de ese framework. Y si algún día lo quito, lo que queda es HTML normal.

Sin clases no quiere decir que no las haya. Mi HTML tiene unas cuantas —site-nav, mobile-bar, meta-table—, pero son mías y las pinta mi CSS: están ahí para colocar cosas, no para que Pico haga su trabajo. De las suyas uso una, container.

El reparto que salió de ahí es el que da título a esto: Pico pone la estética, mi CSS pone el layout. Con una regla dura que no he roto ni una vez — pico.min.css no se toca. Nunca. Lo que haga falta cambiar se cambia desde mis propios archivos, girándole los mandos que él mismo expone. El día que actualice Pico, mi CSS sigue encajando; si le hubiera editado el archivo, actualizar sería volver a hacer el trabajo.

Sostener ese reparto no salió gratis. Hubo que decidir cómo ganarle a Pico sin pelearme con la especificidad, y cómo dejar de tener un único archivo de CSS que ya no se podía leer. Lo cuento más abajo, porque ninguna de las dos decisiones se tomó en frío: las dos las forzó algo concreto que quise poner en el sitio.

La tipografía va al revés que el framework

El primer sitio donde ese reparto se puso a prueba fue el tamaño del texto.

Pico escala la tipografía por el ancho de la ventana: cuanto más grande la pantalla, más grande la letra. Detrás hay una suposición razonable —una pantalla grande se mira desde más lejos— que en este sitio resultó ser falsa. Aquí el texto mide 20px en móvil y 17 en escritorio. El móvil va un 18% más grande, que es mucho, y es a propósito.

Hay dos motivos, y se suman.

El primero es la medida, entendida como caracteres por línea. En escritorio la columna de texto está topada a un ancho de lectura fijo, así que agrandar la letra no ensancha nada: acorta la línea y la acerca a las 72 letras que se buscan. En móvil la columna la fija la pantalla, no la tipografía; ahí el tamaño no compite con nada, solo con cuánto texto cabe.

El segundo es el tamaño angular. Un teléfono se sostiene bastante más cerca que un ordenador, pero sus píxeles son mucho más pequeños, y un píxel de CSS no le llega al ojo con el mismo tamaño en los dos sitios.

Lo interesante no es el número, es cómo apareció. Esto no se ve encogiendo la ventana del navegador. Una ventana estrecha te da el mismo ancho, pero no la misma densidad de pantalla ni la misma distancia de lectura: en el emulador el sitio estaba bien y en el teléfono se leía pequeño. El fallo era invisible en la herramienta con la que estaba mirando.

Antes de tocar nada medí lo único que este cambio puede estropear, que son los caracteres por línea:

ventana 17px (antes) 18px 19px 20px (elegido)
360px 40 33 33 33
390px 42 40 35 33
430px 48 43 42 40

Los números decían 18: deja unas 40 letras por línea, dentro del rango cómodo de 35 a 45 que se cita siempre. El teléfono decía 20. Me quedé con 20, y creo que la tabla estaba mal aplicada más que mal hecha: esa horquilla viene de la tipografía impresa y de pantallas grandes, y en un móvil la línea se recorre de una sola mirada. Lo que cansa ahí no es volver al principio de la siguiente, es forzar la vista.

Y hay algo que solo se aprende probando en el aparato: 18px no era la opción prudente, era la opción invisible. De 17 a 18 hay un 5,9% de diferencia — tan poco que al probarlo parecía que el cambio no había llegado al teléfono. Si un ajuste existe para arreglar algo que molesta y puesto no se nota, se queda corto por definición. El salto a 20 es del 18% y no admite duda.

Todo esto está escrito en mi CSS, en una sola declaración, y en porcentaje para respetar el tamaño que cada visitante tenga configurado en su navegador. El mando es de Pico —es su variable— pero el valor es mío, y su archivo sigue sin tocarse. El escalón, además, cae en los mismos 768px donde el menú pasa de barra inferior a columna: un solo sitio donde el sitio cambia de forma, no dos.

La tabla me sirvió para ver qué me jugaba y para descartar el ajuste que no se notaba. El número lo acabé eligiendo con el teléfono delante.

Las funciones que empujaron el CSS

Un layout de dos columnas se sostiene solo mientras el sitio no tenga nada dentro. Lo que lo mueve son las funciones, y aquí fueron dos.

El selector de tema —claro, oscuro y sistema— tiene tres estados, y el tercero es el importante: no es «oscuro por defecto», es no decidas por mí, haz lo que diga el ordenador. Las tres opciones necesitan sitio y necesitan poder cerrarse, así que va plegado.

El selector de idioma son dos enlaces, ES y EN. La versión inglesa del sitio la mantengo sin saber quién la va a leer, y esa es justamente la razón: si alguien acaba aquí buscando cómo se resuelve algo de esto, que no se choque con el idioma. Cuesta poco y cubre bastante más.

Los dos controles viven en el mismo sitio, juntos: en la barra lateral, bajo el menú, cuando hay columna; y en la barra inferior cuando no la hay. Son los dos ajustes globales del sitio —lo que cambia cómo lo ves, no a dónde vas— y tenerlos separados obligaría a aprenderse dos sitios en vez de uno.

Ninguno de los dos parece gran cosa, y sin embargo el selector de tema es hoy el componente más grande de mi CSS: diecinueve bloques de reglas. Eso, más los índices y la colección de discos, es lo que acabó rompiendo la organización del archivo. Y antes de eso, el escalón de tipografía ya había roto otra cosa: la forma en que le ganaba a Pico.

Quién manda sobre quién

Cambiarle el tamaño al texto —los 20px del móvil contra los 17 del escritorio— fue el primer sitio donde tuve que ganarle a Pico, y le ganaba por el peor motivo posible: mi hoja de estilos se enlazaba después de la suya. Funciona, pero no está declarado en ninguna parte. El día que alguien mueva dos líneas del <head>, el sitio cambia de aspecto y no hay dónde leer por qué.

La salida clásica es escribir selectores más pesados que los suyos. Es la que quería evitar: cuando un selector es largo porque tiene que ganar una pelea, el CSS pasa a contar dos historias mezcladas —qué quiero que se vea y quién le gana a quién— y solo una de las dos me interesa.

Las capas de cascada resuelven eso de raíz. Se declara un orden una sola vez:

@layer pico, sitio;

y a partir de ahí cualquier regla de la capa sitio gana a cualquier regla de la capa pico, por larga que sea la suya y por corta que sea la mía. Los selectores vuelven a describir solo la intención.

Tres cosas que me costaron:

Lo que no está en ninguna capa gana a todo lo que sí lo está. Las capas no son pisos por encima del CSS normal: son un sótano ordenado por debajo. Así que no bastaba con meter lo mío en una capa. Si Pico se quedaba fuera, Pico ganaba siempre y el layout se caía entero. Tienen que entrar los dos.

Un <link rel="stylesheet"> no se puede asignar a una capa. El atributo existe como propuesta y no lo implementa ningún navegador, así que la única forma de meter un archivo entero en una capa es @import. De ahí que exista main.css, un archivo sin una sola regla de estilo: solo declara el orden de mando y carga a los demás.

Y las capas no cambian a dónde apuntas. Pico pone los marcadores de lista con la regla ul li. Escribir list-style: none en el <ul> no funciona por muy alta que esté su capa, porque ahí no hay ningún conflicto que resolver: su regla habla del <li> y la mía hablaba del <ul>. Las capas deciden quién gana cuando dos reglas apuntan al mismo elemento; no te llevan al elemento correcto. Esa me ha mordido tres veces.

El cambio no movió un píxel —quince capturas, cinco páginas por tres anchos, idénticas antes y después—, y esa es exactamente la descripción de lo que hace: no cambia el aspecto, quita una fragilidad.

Un archivo que ya no se podía leer

Mi CSS llegó a 2.354 líneas en un solo archivo. No creció por descuido: creció porque el sitio fue ganando cosas, y cada una traía las suyas.

Está partido en ocho archivos, uno por componente, y la lista de @import de main.css es el mapa: los tokens, la tipografía, las utilidades, el selector de tema, lo que le apago a Pico, los índices, la colección y el layout.

El orden de esa lista es significativo, no alfabético. Dentro de una misma capa vuelven a mandar las reglas de siempre, y entre dos declaraciones de igual peso gana la última. Así que la lista conserva el orden que tenían las secciones dentro del archivo grande, y por eso partirlo no cambió nada. Mover un @import de sitio sí puede cambiarlo.

Lo difícil no fue repartir, fue decidir las costuras. ¿Dónde va una regla como .mobile-bar .theme-menu, que nombra dos componentes a la vez?

Un selector así se lee de izquierda a derecha: dice «el selector de tema cuando está dentro de la barra inferior». Y lo que decide esa regla no es cómo es el selector de tema, sino dónde lo coloca la barra. Es una regla de la barra, así que va con el layout. De ahí el criterio: una regla vive con el primer elemento de su selector.

Como el cambio solo movía código, la prueba podía ser exigente: el CSS minificado tenía que salir byte a byte idéntico. Al minificar desaparecen los comentarios, así que lo único que puede diferir es CSS de verdad. Salieron los mismos 95.410 bytes.

Y partir el archivo destapó algo que no buscaba: una convención mía que llevaba tiempo sin cumplirse. Modularizar no creó ese fallo, lo hizo visible — que era justamente uno de los motivos para partir el archivo.

Una fuente que costó 8.584 bytes

La interfaz y los titulares del sitio no siempre fueron esta fuente. Eran Figtree, y el cambio a Source Sans 3 no salió de la fuente, sino de un rediseño.

Cuando la jerarquía la llevaba el tamaño —el titular de un artículo llegaba a medir 49px—, la familia apenas se notaba, así que elegir una u otra era casi decorativo. Al aplanar la escala, la familia pasó a ser la señal principal, y Figtree se encontró sosteniendo sola un papel para el que nadie la había elegido.

El criterio no cambió, que es lo interesante: sigue siendo contraste sin choque, dos tipografías humanistas que contrastan solo en el género, sans contra serif. Source Sans 3 es humanista igual que Figtree. Lo que cambia es el carácter, que ahora se ve porque ya no lo tapa el tamaño. Se compararon seis candidatas en el sitio de verdad, no en muestras sueltas.

Y tiene un precio que conviene decir en voz alta: el archivo latin pasa de 20.156 a 28.740 bytes. 8.584 bytes más, un 42%. Se acepta a sabiendas —es la fuente de toda la interfaz y se descarga una vez—, pero es una decisión con factura, no una preferencia.

Las imágenes se optimizan solas

El objetivo era no preparar nada antes de subir una foto: arrastrarla al directorio del artículo y ya. Lo hace el plugin de imágenes de Eleventy, que genera las variantes al construir el sitio y escribe el <img> con todos los anchos.

Medido con una foto de 4032×3024 y 6,1 MB: un móvil se descarga 2,3 KB. El original no llega a publicarse siquiera, porque el plugin lo lee del directorio fuente y no de la salida.

Los dos defaults que hubo que corregir dicen bastante sobre fiarse de la configuración por defecto:

  • widths incluía el ancho original. Con él, esa foto generaba también las variantes de 4032px: 8,6 MB que nadie descarga, porque el sitio nunca pinta una imagen a más de 830px. Se comían 43 de los 43 segundos del build; sin ellas, 12,2.
  • sizes="auto" solo lo entiende Chrome 133 y posteriores. Los demás caen al valor por defecto, 100vw, y en un monitor de 1920 eso pide la variante más grande para pintarla a 830px. Los anchos hubo que medirlos en el sitio servido: el ancho de lectura depende de la fuente, así que no se puede calcular sobre el papel.

Todo esto suena muy ordenado

Y no lo fue. Elegir un framework por lo que te da tiene una contrapartida que no se ve hasta que trabajas con él: también te da lo que no le has pedido, y casi siempre te enteras tarde.

<article> no significa «un artículo»

Pico decora por elemento, no por clase. Esa es su gracia y también su trampa: <article> no es «un artículo» para él, es su componente de tarjeta. Eliges la etiqueta por lo que significa y te llega decorada por cómo se ve.

El criterio para salir de ahí acabó siendo una pregunta: ¿este elemento me da algo más que su aspecto? Si no, cambio de elemento. Si sí, me quedo y le apago la decoración. A veces solo estorba una propiedad: al código en línea le dejé el fondo, porque distinguir el código del texto es información, no adorno.

Y apagar la tarjeta trajo lo que no esperaba. Pico saca la cabecera y el pie de un <article> a sangre con márgenes negativos, compensados por el relleno de la tarjeta. Al quitar el relleno, la compensación se quedó sin nada que compensar y la banda gris se salía un rem por cada lado. La lección es transportable: al desactivar una decoración, mirar qué otras reglas se apoyaban en ella.

Ocho píxeles que no eran míos

El sitio tuvo barra de scroll horizontal a partir de 768px desde que monté el layout, y no lo vi nunca.

La causa es el fallo clásico de las bandas a todo el ancho. La cuenta que las saca del margen era calc(50vw - 50%), y esas dos unidades no miden lo mismo: 50vw cuenta la barra de scroll vertical y 50% no. La diferencia es medio ancho de barra por lado, unos 8 píxeles en total.

Por qué no se veía: el texto no se desalinea. El margen negativo y el relleno llevan los dos la misma cuenta y se cancelan. Lo único que sobresale es fondo.

Probé dos arreglos y los descarté los dos, cada uno por un motivo distinto que merecía la pena entender antes que insistir. El que quedó es recortar lo que sobra con overflow-x: clip, y aquí recortar es legítimo precisamente porque lo que se sale es solo fondo. clip y no hidden: hidden crea un contenedor de scroll y rompe cualquier position: sticky que haya dentro.

Lo que más me sirvió de este fallo no fue el arreglo. Fue que mi herramienta de comparar capturas no podía verlo: daba 96 imágenes idénticas antes y después, porque lo que sobresalía caía fuera del recorte. Un comparador de imágenes no es un validador de maquetación.

Ganar de más

Las capas resuelven la especificidad quitándola de la ecuación, y eso corta por los dos lados. Quise apagarle a Pico una decoración del código en línea con un code a secas. Como la capa sitio gana sin mirar la especificidad, esa regla de cuatro letras también le ganaba a pre > code de Pico y convertía los bloques de código en texto corrido.

No lo cacé yo: me lo corrigió el hecho de mirar el sitio, donde los bloques ya se veían bien. El arreglo es acotar el selector, :not(pre) > code. La lección general es la que se le escapa a cualquiera que descubre las capas: cuando la capa te da la victoria en todo lo que case, el selector pasa a ser el único freno.

Dónde queda la frontera

Ninguna de estas decisiones fue sobre estética. Pico sigue poniendo los colores y el modo claro y oscuro, y su archivo sigue sin tocarse. Lo que fui aprendiendo es otra cosa: dónde tenía que estar escrito cada cambio para que se pudiera volver a leer. Las capas para no ganar por accidente, los ocho archivos para poder encontrar algo, los tokens para que un valor viva en un solo sitio.

Los tres fallos de arriba salieron del mismo sitio, y no es Pico: es que un framework toma decisiones que tú no has tomado, y no las anuncia. Mientras el reparto esté escrito, esas decisiones se pueden mirar de frente. El día que hubiera empezado a editar pico.min.css, habría dejado de haber reparto que mirar.

← Volver a los artículos

Jestemrique

Desarrollo, proyectos, aprendizaje…

Tema