grow y shrink: la asimetría que nadie explica
flex-grow reparte el espacio libre en proporción directa, pero flex-shrink lo hace ponderado por el tamaño base. Las dos fórmulas con números, y por qué esa asimetría es la correcta.
flex-grow y flex-shrink parecen simétricos: uno reparte lo que sobra y el otro lo que falta. No lo son. flex-grow distribuye en proporción directa a su valor; flex-shrink distribuye en proporción a su valor multiplicado por el tamaño base del elemento. Esa multiplicación es la diferencia entre predecir un layout y adivinarlo, y está en la especificación desde el primer día sin que casi ningún tutorial la mencione.
- Calcular el reparto de espacio libre positivo con
flex-grow. - Calcular el reparto de déficit con
flex-shrinkponderado por la base. - Justificar por qué el encogimiento se pondera y el crecimiento no.
- Predecir el resultado de un contenedor con valores mixtos sin ejecutarlo.
El espacio libre
El algoritmo empieza sumando los tamaños hipotéticos de todos los elementos y restándolos del tamaño del contenedor en el eje principal, gaps incluidos. El resultado es el espacio libre.
Si es positivo, sobra sitio y actúa flex-grow. Si es negativo, falta sitio y actúa flex-shrink. Nunca actúan los dos: la rama del algoritmo se elige una vez y no se revisa.
espacio libre = tamaño del contenedor
- suma de tamaños hipotéticos
- suma de gaps
Que el signo decida la rama tiene una consecuencia práctica inmediata: flex-shrink no hace absolutamente nada si tus elementos caben, y flex-grow no hace nada si no caben. Poner los dos, como hace flex: 1 1 auto, es simplemente cubrir los dos casos.
flex-grow: proporción directa
Cuando el espacio libre es positivo, cada elemento recibe una parte proporcional a su flex-grow respecto a la suma de todos los flex-grow:
reparto del elemento i = espacio libre × ( grow_i / suma de grows )
tamaño final_i = base_i + reparto_i
Contenedor de 600px. Tres elementos con base 100 cada uno, y flex-grow de 1, 2 y 3.
Los tamaños hipotéticos suman 300, así que el espacio libre es 300. La suma de grows es 6. El primero recibe 300 × 1/6 = 50, el segundo 300 × 2/6 = 100, el tercero 300 × 3/6 = 150.
Tamaños finales: 150, 200 y 250. Fíjate en que no están en proporción 1:2:3, porque cada uno partía de una base de 100. Solo el reparto está en esa proporción.
.contenedor { display: flex; inline-size: 600px; }
.contenedor > * { flex-basis: 100px; }
.uno { flex-grow: 1; } /* acaba en 150px */
.dos { flex-grow: 2; } /* acaba en 200px */
.tres { flex-grow: 3; } /* acaba en 250px */
Para que las proporciones finales sean 1:2:3, la base tiene que ser cero. Ese es otro de los efectos de flex: 1 que conviene tener presente: con base cero, el reparto es el tamaño total y las proporciones se cumplen exactamente.
Si la suma de todos los flex-grow es menor que 1, el reparto se hace sobre esa fracción y queda espacio sin repartir. Con dos elementos a flex-grow: 0.25 cada uno, se reparte solo la mitad del espacio libre y el resto se queda como hueco al final. Es una forma poco conocida de repartir parcialmente sin recurrir a justify-content.
flex-shrink: proporción ponderada
Aquí está la asimetría. Cuando el espacio libre es negativo, el factor de cada elemento no es su flex-shrink a secas, sino flex-shrink × tamaño base:
peso_i = shrink_i × base_i
recorte del elemento i = |espacio libre| × ( peso_i / suma de pesos )
tamaño final_i = base_i - recorte_i
Contenedor de 400px. Dos elementos, uno con base 100 y otro con base 300, ambos con flex-shrink: 1.
Los hipotéticos suman 400, cabe justo, no hay déficit. Estrechemos el contenedor a 300px: ahora el déficit es 100.
Los pesos son 1 × 100 = 100 y 1 × 300 = 300, que suman 400. El primero cede 100 × 100/400 = 25. El segundo cede 100 × 300/400 = 75.
Tamaños finales: 75 y 225. El elemento grande ha cedido tres veces más que el pequeño, con el mismo flex-shrink.
.contenedor { display: flex; inline-size: 300px; }
.pequeno { flex: 0 1 100px; } /* acaba en 75px */
.grande { flex: 0 1 300px; } /* acaba en 225px */
Si no hubiera ponderación, cada uno habría cedido 50: el pequeño se habría quedado en 50 y el grande en 250. Es decir, el pequeño habría perdido la mitad de su tamaño y el grande solo un sexto. Visualmente eso es un desastre: los elementos pequeños desaparecen antes que los grandes.
Y esa es la clave: la ponderación no es un detalle de implementación, es la respuesta a un problema real de diseño.
Cuando sobra espacio, repartirlo por igual es razonable: nadie sale perjudicado y el reparto proporcional al factor deja que expreses prioridades. Un elemento que crece el doble que otro simplemente ocupa más del extra.
Cuando falta espacio, repartir por igual es destructivo. El daño relativo que sufre un elemento al ceder 50px depende de lo grande que sea: para uno de 300px es un recorte del 17%, para uno de 60px es del 83%, y probablemente ilegible. Ponderar por la base iguala el daño relativo, no el absoluto: cada elemento pierde aproximadamente el mismo porcentaje de sí mismo.
Con la fórmula ponderada, dos elementos con el mismo flex-shrink acaban perdiendo la misma proporción de su tamaño, que es exactamente lo que un diseñador esperaría al estrechar una ventana.
El valor inicial de flex-shrink es 1, lo que significa que todo elemento flex encoge por defecto. Los iconos, los avatares, los botones de ancho fijo y las etiquetas cortas suelen no querer eso. flex-shrink: 0, o su forma corta flex: none, es la declaración que más veces salva un layout de barra de herramientas.
Valores mixtos
Con lo anterior puedes resolver cualquier caso. Ejemplo completo: barra con un logotipo que no debe encogerse, un buscador que se lleva todo el espacio y dos botones de tamaño fijo.
.barra {
display: flex;
gap: 1rem;
align-items: center;
inline-size: 100%;
}
.logo { flex: 0 0 auto; } /* ni crece ni encoge, tamaño de contenido */
.buscador{ flex: 1 1 12rem; } /* crece, encoge, pero parte de 12rem */
.accion { flex: 0 0 auto; } /* fijo */
El flex: 1 1 12rem del buscador merece atención: con espacio de sobra crece hasta llenar todo lo que dejen los demás; con espacio justo encoge desde 12rem. Y como los demás tienen flex-shrink: 0, todo el déficit recae sobre él, que es exactamente lo que quieres: prefieres un buscador estrecho a un logotipo aplastado.
Detrás de la fórmula ponderada hay una tesis de diseño que vale la pena extraer, porque reaparece en sitios donde no la esperas. Al repartir abundancia, CSS es proporcional al factor que tú declaras: mandas tú. Al repartir escasez, CSS es proporcional al tamaño, es decir, protege por defecto a los elementos pequeños y hace que el daño sea perceptualmente uniforme. La especificación toma una decisión ética sobre quién sufre cuando no hay para todos, y la toma en tu favor. Ese mismo patrón está por todas partes en el layout moderno una vez que sabes buscarlo. El mínimo automático de un elemento flex existe para que el contenido no desaparezca cuando aprietas. minmax() en grid separa explícitamente el comportamiento en abundancia del comportamiento en escasez. clamp() es literalmente eso: un valor preferido, un suelo para cuando escasea y un techo para cuando sobra. Todo el vocabulario que CSS ha añadido en la última década consiste en darte control separado sobre los dos regímenes, porque la lección aprendida de veinte años de layouts rotos es que un solo número no puede describir a la vez cómo se comporta algo cuando hay sitio de sobra y cuando no lo hay. Si te llevas una sola idea de este nivel, que sea esta: especifica siempre los dos regímenes, y si solo especificas uno, asegúrate de saber qué está haciendo el motor en el otro.