wandres.dev
UNIDADES DE CONTENEDOR · cqw, cqi, cqmin y compañía

Espaciado y ritmo proporcionales al contenedor

Una sola custom property que gobierna paddings, gaps y radios de todo un componente, qué debe escalar y qué no, y por qué el ritmo vertical necesita un tratamiento distinto.

⏱ 16 min

El tamaño de letra no es lo único que hace que un componente se vea desproporcionado fuera de su tamaño de diseño. Un padding de 24px es generoso en una tarjeta de 300px y mezquino en una de 900px; un gap fijo convierte una tarjeta grande en una lista de bloques flotando en el vacío. Escalar el espaciado con el contenedor es la mitad menos glamurosa del componente portable y la que más diferencia produce, siempre que sepas distinguir qué se escala y qué debe quedarse quieto.

🎯 Al terminar esta lección sabrás
  • Derivar todo el espaciado de un componente de un único paso base en cqi.
  • Clasificar cada medida del componente en escalable, constante o dependiente del usuario.
  • Construir una escala de espaciado con razón constante sin repetir clamp() en cada valor.
  • Tratar el ritmo vertical de forma coherente con el horizontal.

Un paso base y todo lo demás derivado

La técnica consiste en declarar una sola medida fluida y expresar el resto como múltiplos suyos:

.tarjeta {
  --paso: clamp(0.75rem, 0.5rem + 1.2cqi, 1.75rem);

  padding: var(--paso);
  display: grid;
  gap: var(--paso);
  border-radius: calc(var(--paso) * 0.6);
}

.tarjeta__acciones { gap: calc(var(--paso) * 0.5); margin-block-start: calc(var(--paso) * 0.25); }
.tarjeta__media    { margin-inline: calc(var(--paso) * -1); }  /* sangrado a los bordes */

Dos ventajas concretas. La primera, que el componente se ajusta cambiando un número, lo que hace posible ofrecer variantes de densidad sin duplicar reglas. La segunda, que las proporciones internas se conservan exactamente en todos los tamaños: el radio siempre es seis décimas del padding, el hueco de las acciones siempre la mitad del principal.

Ese sangrado negativo del último ejemplo es un caso donde la técnica brilla: una imagen que llega a los bordes de la tarjeta necesita un margen negativo exactamente igual al padding, y cuando el padding es fluido, la única forma de que sigan cuadrando es derivar uno del otro.

Escala con razón constante

Si prefieres una escala de varios pasos, no repitas clamp() en cada uno. Deriva todos del mismo paso base multiplicando por potencias de una razón:

.componente {
  --paso: clamp(0.75rem, 0.5rem + 1.2cqi, 1.75rem);
  --e-0: calc(var(--paso) * 0.25);
  --e-1: calc(var(--paso) * 0.5);
  --e-2: var(--paso);
  --e-3: calc(var(--paso) * 1.5);
  --e-4: calc(var(--paso) * 2.25);
  --e-5: calc(var(--paso) * 3.375);
}

Los factores 1, 1.5, 2.25 y 3.375 son potencias de 1.5, la misma idea de razón constante que usarías en una escala tipográfica modular. Al derivar todo de --paso, la escala entera es fluida con una sola expresión, y una variante compacta se consigue redefiniendo solo esa.

.componente.compacto { --paso: clamp(0.5rem, 0.4rem + 0.6cqi, 1rem); }

Qué no debe escalar

Aquí está el criterio que separa un componente cuidado de uno donde todo se ha multiplicado por lo mismo.

Los grosores ópticos no escalan. El borde de 1px es 1px en todos los tamaños. Un borde que crece a 3px en tarjetas grandes deja de leerse como borde y empieza a leerse como marco. Lo mismo con el anillo de foco, que además tiene requisitos de contraste y de grosor mínimo.

Los objetivos táctiles no escalan hacia abajo. Un botón debe tener al menos unos 24px de lado según el criterio de tamaño de objetivo de las WCAG 2.2, y 44px si sigues las guías de las plataformas móviles. Ese mínimo es del dedo, no del contenedor.

.tarjeta__boton {
  min-block-size: 2.75rem;                    /* del dedo, en rem */
  padding-inline: calc(var(--paso) * 0.75);   /* del contenedor */
  border: 1px solid;                          /* constante */
}

El texto de lectura no escala con el contenedor más allá de un ajuste mínimo, por lo que ya vimos: manda el usuario. Un componente que reduce el cuerpo de texto en contenedores estrechos está tomando una decisión sobre legibilidad basándose en el dato equivocado.

⚠️
El error de escalar el interlineado de párrafo con cqi

Es tentador hacer que todo el ritmo vertical sea proporcional, incluida la separación entre párrafos de un texto largo. No lo hagas: la separación entre párrafos debe ser proporcional al tamaño de la fuente, porque es lo que la vista usa para agrupar. Exprésala en em y déjala en paz. El --paso en cqi es para el espaciado estructural del componente —padding de la caja, huecos entre bloques, márgenes de las zonas—, no para el ritmo tipográfico.

Ritmo vertical

El eje de bloque necesita un tratamiento aparte por un motivo práctico: el contenedor típico es inline-size, así que no tienes cqb disponible. Escalar los márgenes verticales con cqi funciona y es lo que se hace, pero conviene ser consciente de que estás usando el ancho para dimensionar espacio vertical.

En la práctica es la decisión correcta más veces de lo que parece, porque un contenedor ancho suele implicar más espacio también en vertical y porque el ojo compara el padding vertical con el horizontal. Lo que sí conviene es usar factores distintos:

.seccion {
  --paso: clamp(0.75rem, 0.5rem + 1.2cqi, 1.75rem);
  padding-inline: var(--paso);
  padding-block: calc(var(--paso) * 1.5);   /* mas aire arriba y abajo */
  margin-block: calc(var(--paso) * 2);
}

Y si de verdad necesitas proporción vertical real, el contenedor tiene que ser container-type: size con altura determinada, y entonces cqb está disponible. Es el caso de los paneles de altura fija de un tablero, no el de una tarjeta de contenido.

Un componente es un sistema de proporciones, no una lista de medidas

La razón profunda por la que este enfoque funciona no es que ahorre declaraciones. Es que un diseño se percibe por relaciones, no por valores absolutos: el ojo no ve “24px de padding”, ve “el padding es el doble que el hueco” y “el radio es una cuarta parte del padding”. Cuando escribes cada medida como un número independiente, esas relaciones existen solo en la maqueta de la que copiaste los números, y se destruyen en cuanto una de las medidas cambia por cualquier motivo: un ajuste de última hora, un tamaño de contenedor que nadie previó, una variante de densidad. Derivar todo de un paso base convierte las relaciones en lo declarado y los valores en lo derivado, que es exactamente la inversión que hace que un sistema de diseño sea un sistema y no una colección. La prueba de si lo has hecho bien es directa y vale la pena aplicarla: cambia el paso base por otro cualquiera y mira si el componente sigue estando bien compuesto. Si lo está, has capturado el diseño; si se descuadra algo, ese algo era una relación que no habías declarado.