Tipografía fluida con clamp() CSS

Tabla de contenido

Construir una interfaz dinámica con animaciones y responsive a partir de un archivo de Figma es como dar vida al monstruo de Frankenstein, es fácil acabar con algo descontrolado, e inconsistente causado por elementos que no encajan bien, y en parte es por culpa de haber tenido que interpretar el diseño.

El flujo de trabajo

Estamos acostumbrados a recibir un Figma con “capturas” de una interfaz, y aunque se obtiene mucha información de los elementos que la componen y además puede haber prototipos que ilustran el estilo de las animaciones, ¿qué pasa con las resoluciones intermedias?

No es viable adaptar el diseño a muchos o a todos los tamaños de dispositivos diferentes, y por eso toca intrepretar qué ocurre en las zonas grises.

1440px es casi el doble que 768px y tiene sentido que un h1 crezca de 30px a 80px por ejemplo, pero ¿qué ocurre a 1024px? ¿Y a 1200px? ¿A 1339px sigue midiendo 30px?

Utilizar múltiples puntos de corte es repetitivo y verboso, así que lo ideal sería que se pudiera ajustar el tamaño en función del ancho del dispositivo sin tener que re-resetear el valor cuando se alcanza un nuevo punto de corte.

Clamp()

Recibe 3 parámetros: valor mínimo, valor ideal y valor máximo, y la función aplica el valor ideal siempre que no exceda de sus límites.

📚 Si necesitas repasar cómo funciona clamp() escribimos una artículo sobre esto

Tipografía fluida

Para que fluya tenemos que usar unidades relativas y como queremos que lo haga respecto al viewport, elegiremos vw.

Para un encabezado de 30px a 375px y de 80px a 1440px ya tenemos sus valores mínimo y máximo, y el valor ideal podría ser 7vw, por ejemplo, porque a simple vista parece que encaja bien → clamp(30px, 7vw, 80px).

Sin embargo, 7vw es arbitrario y puede dar resultados inesperados, así que para ajustarlo un poco podemos calcular la proporción en un punto concreto, por ejemplo a 1440px → 80 / 1440 * 100 = 5.5 → transformarlo a CSS → 5.5vw y aplicarlo → clamp(30px, 5.5vw, 80px) y aunque ahora está mejor, no encajará bien porque en el otro extremo la relación es diferente → 32 / 375 * 100 = 8.53 y por tanto con un valor 5.5vw el encabezado alcanzará los 30px por debajo de 375px en cambio si usamos el valor mínimo → clamp(30px, 8.53vw, 80px) alcanzaremos los 80px antes de los 1440px.

Progresión lineal

El problema es que buscamos un crecimiento lineal para un rango dado pero la relación font-size / width entre los puntos conocidos no es igual: 80 / 1440 * 100 = 5.5 y 32 / 375 * 100 = 8.53 y el valor que hemos aproximado obviamente no sirve, pero conociendo dos puntos podemos trazar una línea recta entre ellos.

Gráfico de coordenadas que muestra la relación entre el tamaño de la pantalla y el tamaño de un texto
📚 Hay calculadoras online pero aprender a calcularlo por uno mismo es más nutritivo, se integra mejor y se elimina la magia, además, abre la puerta a otras ideas por explorar

La fórmula general para la ecuación de una recta es D = aT + b donde a es la pendiente (velocidad de cambio), b es el intercepto (valor cuando T es 0)

El cálculo de la pendiente es → a = D2​−D1​ / T2​−T1​​ y b = D1 - aT1 para obtener el intercepto. T es el ancho del viewport en píxeles reales pero hay que convertirlo a vw porque es la unidad que entiende CSS → 1vw = 100T ​⇒ T = 100vw.

# (375, 30), (1440, 80)

a = (80 - 30) / (1440 - 375)
a = 50 / 1065
a ≈ 0.046948

b = 30 - a * 375
b = 30 - (0.046948 * 375)
b = 30 - 17.6055
b ≈ 12.3945

D(w) = 0.046948w + 12.3945

w = 100vw

D = 0.046948(100vw) + 12.3945
D = 4.6948vw + 12.3945px

# CSS:
calc(4.695vw + 12.39px)

# Clamp:
# Dentro de clamp() se puede eliminar calc()
clamp(30px, (12.39px + 4.695vw), 80px)

Así que 12.39px + 4.695vw es el valor exacto que buscábamos, nada de aproximaciones 🔥 De este modo podemos asegurar que se aplica el tamaño mínimo a la resolución menor y pero que crecerá progresivamente hasta llegar al punto de corte indicado, momento en el que tendrá el mayor tamaño.

Otras aplicaciones

Podemos usarlo para cualquier propiedad CSS que acepte un valor de tipo longitud <length>, por ejemplo padding si queremos que el margen interior de un contenedor crezca conforme lo hace el ancho del viewport.

BONUS - @function

Hacer los cálculos manualmente puede ser repetitivo, y aunque no está totalmente soportado, podríamos aprovechar @function para crear una función que calcule la escala.

@function --a(--D1, --D2, --T1, --T2) {
  result: calc((var(--D2) - var(--D1)) / (var(--T2) - var(--T1)));
}

@function --b(--D1, --D2, --T1, --T2) {
  result: calc(var(--D1) - (var(--D2) - var(--D1)) / (var(--T2) - var(--T1)) * var(--T1));
}


h1 {
  /* D = ((D2 - D1) / (T2 - T1)) * T + (D1 - ((D2 - D1) / (T2 - T1)) * T1) */
  --D1: 30px;
  --D2: 120px;
  --T1: 600px;
  --T2: 1440px;
  
  --a: --a(var(--D1), var(--D2), var(--T1), var(--T2));
  --b: --b(var(--D1), var(--D2), var(--T1), var(--T2));
  --fx: var(--a) * 100vw + var(--b);
  
  font-size: clamp(var(--D1), var(--fx), var(--D2));
}

Echa un vistazo a la demo completa:

Conclusión

Seguirá habiendo puntos ciegos, zonas grises, intenciones no implícitas y por tanto invisibles en los materiales con los que trabajemos pero con esta técnica podemos ayudar a que nuestro monstruo se vea más orgánico y natural. Un poco más vivo.

Recursos

comments powered by Disqus

Si te ha parecido interesante

Tanto si tienes alguna duda o te apetece charlar sobre este tema, así como si el contenido te parece interesante o crees que pdemos hacer algo juntos, no dudes en ponerte en contacto con nosotros a través del email hola@mamutlove.com