Columnas iguales con Flexbox: es más complicado de lo que piensas

Te entregan un diseño atractivo y tiene esta bonita sección de gran héroe, seguida de una de esas tres columnas justo debajo. Ya sabes, como casi todos los demás sitios web en los que ha trabajado.
Pasas por la sección de héroes y te pones a trabajar en la sección de tres columnas. ¡Es hora de sacar a nuestro fiel amigo flexbox! Excepto que escribes display: flex
y obtienes este desorden en lugar de obtener las tres columnas iguales que esperarías.
Esto sucede debido a cómo flexbox calcula el tamaño base de un elemento . Probablemente hayas leído muchos tutoriales de Flexbox, y muchos de ellos (incluido el mío) son un ejemplo demasiado simplista de lo que hace Flexbox sin profundizar realmente en las cosas complejas que Flexbox hace por nosotros y que damos por sentado.
Estoy seguro de que has visto ejemplos y tutoriales que analizan algo como esto, donde tres divs se reducen alrededor del contenido que hay dentro de ellos:
En otras palabras, obtenemos algunos elementos a nivel de bloque que se reducen y se ubican uno al lado del otro. Parece que Flex quiere que las cosas sean lo más pequeñas posibles. Pero en realidad, flexbox quiere que las cosas sean lo más grandes posibles.
¿Esperar lo? Flex reduce las cosas de forma predeterminada: ¡eso no puede estar bien! ¿Bien?
Por increíble que sea flexbox, lo que hace bajo el capó es en realidad un poco extraño porque, de forma predeterminada, hace dos cosas a la vez. Primero analiza el tamaño del contenido, que es lo que obtendríamos si declaramos width: max-content
un elemento. Pero además de eso, flex-shrink
también se está trabajando para permitir que los artículos sean más pequeños, pero solo si es necesario.
Para entender realmente lo que está pasando, analicemos dos de ellos y veamos cómo funcionan juntos.
sumergirse enmax-content
max-content
Es un valor de propiedad bastante útil en determinadas situaciones, pero queremos entender cómo funciona en esta situación particular en la que intentamos obtener tres columnas iguales aparentemente simples. Así que dejemos de lado flexbox por un momento y veamos qué max-content
funciona por sí solo.
MDN hace un buen trabajo al explicarlo:
La
max-content
palabra clave de tamaño representa el ancho máximo intrínseco del contenido. Para el contenido de texto, esto significa que el contenido no se ajustará en absoluto incluso si provoca desbordamientos.
Intrínseco puede confundirte aquí, pero básicamente significa que dejamos que el contenido decida el ancho, en lugar de establecer explícitamente un ancho establecido. Uri Shaked describe con certeza el comportamiento diciendo que “el navegador finge que tiene un espacio infinito y coloca todo el texto en una sola línea mientras mide su ancho”.
Entonces, en resumen, max-content
permite que el contenido mismo defina el ancho del elemento. Si tiene un párrafo, el ancho de ese párrafo será el texto dentro de él sin saltos de línea. Si es un párrafo lo suficientemente largo, terminarás con un desplazamiento horizontal.
Volvamos a ese ejemplo demasiado simplista de tres elementos a nivel de bloque que se encogen y se ubican uno al lado del otro. Eso no está sucediendo debido a flex-shrink
; sucede porque ese es el tamaño de esos elementos cuando su ancho declarado es max-content
. Eso es literalmente lo más amplio posible porque es tan amplio como el contenido combinado dentro de cada elemento.
Aquí, eche un vistazo a esos elementos sin flexbox haciendo sus cosas de flexbox, pero con un width: max-content
ahí en su lugar:
Entonces, cuando hay solo una pequeña cantidad de texto, lo intrínseco max-content
reduce las cosas en lugar de flex-shrink
. Por supuesto, flexbox también viene con su flex-direction: row
comportamiento predeterminado que convierte los elementos flexibles en columnas, colocándolos uno al lado del otro. Aquí hay otro aspecto pero con el espacio libre resaltado.
Sumando flex-shrink
a la ecuación
Entonces vemos que la declaración display: flex
impulsa ese max-content
tamaño intrínseco en los elementos flexibles. Esos elementos quieren ser tan grandes como su contenido. Pero hay otro valor predeterminado que también aparece aquí, que es flex-shrink
.
flex-shrink
Básicamente se trata de observar todos los elementos flexibles en un contenedor flexible para asegurarse de que no se desborden el contenedor principal . Si todos los elementos flexibles pueden caber uno al lado del otro sin desbordar el contenedor flexible, entonces flex-shrink
no servirán de nada… el trabajo ya está hecho.
Pero si los elementos flexibles desbordan el contenedor (gracias a ese max-content
ancho intrínseco), se permite que los elementos flexibles se encojan para evitar ese desbordamiento porque flex-shrink
se está cuidando de eso.
Es por eso que flex-shrink
tiene un valor predeterminado de 1
. Sin él, las cosas se complicarían bastante rápido.
He aquí por qué las columnas no son iguales
Volviendo a nuestro escenario de diseño donde necesitamos tres columnas iguales debajo de un héroe, vimos que las columnas no tienen el mismo ancho. Esto se debe a que flexbox comienza observando el tamaño del contenido de cada elemento flexible antes incluso de pensar en reducirlo.
En aras de la simplicidad, a medida que profundizamos en esto, trabajemos con algunos números redondos agradables. Podemos hacer esto declarando anchos en nuestros elementos flexibles. Cuando declaramos un ancho en un elemento flexible, descartamos ese tamaño intrínseco, ya que ahora hemos declarado un valor explícito . Esto hace que sea mucho más fácil descubrir qué está pasando realmente.
En el bolígrafo a continuación, tenemos un padre que es un 600px
contenedor ancho y flexible ( display: flex
). Eliminé todo lo que pudiera influir en los números, así que no gap
o padding
. También cambié el botón border
por an outline
para que aún podamos visualizar todo fácilmente.
El primer y tercer elemento flexible tienen a width: 300px
y el del medio a width: 600px
. Si sumamos todo eso, es un total de 1200px
. Eso es más grande que el 600px
disponible dentro del padre, así que flex-shrink
entra en acción.
flex-shrink
es una proporción. Si todo tiene lo mismo flex-shrink
(que es 1
lo predeterminado), todos se encogen al mismo ritmo. Eso no significa que todos se encojan al mismo tamaño o en la misma cantidad , pero todos se encogen al mismo ritmo.
Si volvemos a Firefox DevTools, podemos ver el tamaño base, el flex-shrink
y el tamaño final. En este caso, los dos 300px
elementos son ahora 150px
y el 600px
uno es ahora 300px
.
Si sumamos todos los tamaños base de los tres elementos flexibles (los anchos reales que declaramos en ellos), el total resulta 1200px
. Nuestro contenedor flexible es 600px
ancho. Si dividimos todo entre 2, ¡entra! Todos se están reduciendo al mismo ritmo, dividiendo su propio ancho por 2 .
No es frecuente que tengamos números redondos como esos en el mundo real, pero creo que esto hace un buen trabajo al ilustrar cómo flexbox hace lo que hace al determinar qué tan grandes deben ser las cosas.
Hacer que las columnas sean iguales
Hay algunas formas diferentes de hacer que las tres columnas que queremos tengan el mismo ancho, pero algunas son mejores que otras. Para todos los enfoques, la idea básica es que queremos que el tamaño base de todas las columnas sea el mismo . Si tienen un tamaño de base igual, entonces se reducirán (o crecerán, si usamos flex-grow
) a la misma velocidad cuando flexbox flexione las cosas y, en teoría, eso debería hacer que tengan el mismo tamaño.
Hay algunas formas comunes de hacer esto, pero como descubrí mientras profundizaba en todo esto, he llegado a creer que esos enfoques son defectuosos. Veamos dos de las soluciones más comunes que veo utilizadas en la naturaleza y explicaré por qué no funcionan.
Método 1: usarflex: 1
Una forma de intentar que todos los elementos flexibles tengan el mismo tamaño base es declarando flex: 1
en todos ellos:
.flex-parent { display: flex; }.flex-parent * { flex: 1; }
En un tutorial que hice una vez, utilicé un enfoque diferente y debieron haber 100 personas preguntándome por qué no lo estaba usando flex: 1
en su lugar. Respondí diciendo que no quería sumergirme en la flex
propiedad taquigráfica. Pero luego lo usé flex: 1
en una nueva demostración y, para mi sorpresa, no funcionó .
Las columnas no eran iguales.
La columna del medio aquí es más grande que las otras dos. No es una tonelada, pero todo el patrón de diseño que estoy creando es para que tengas columnas perfectamente iguales cada vez, independientemente del contenido.
Entonces, ¿por qué no funcionó en esta situación? El culpable aquí es elpadding
componente de la columna del medio.
Y tal vez dirás que es una tontería agregar relleno a uno de los elementos y no a los demás, o que podemos anidar cosas (ya llegaremos a eso). Sin embargo, en mi opinión, debería poder tener un diseño que funcione independientemente del contenido que le pongamos. Después de todo, la Web se trata de componentes que podemos conectar y usar hoy en día.
Cuando configuré esto por primera vez, estaba seguro de que funcionaría y ver aparecer este problema me hizo querer saber qué estaba pasando realmente aquí.
La flex
taquigrafía hace más que simplemente configurar el archivo flex-grow: 1
. Si aún no lo sabes, flex
es una abreviatura de flex-grow
, flex-shrink
y flex-basis
.
Los valores predeterminados para esas propiedades constituyentes son:
.selector { flex-grow: 0; flex-shrink: 1; flex-basis: auto;}
No voy a profundizar flex-basis
en este artículo porque es algo que Robin ya ha hecho bien. Por ahora, podemos pensar en width
mantener las cosas simples ya que no estamos jugando con flex-direction
.
Ya hemos visto cómo flex-shrink
funciona. flex-grow
es lo opuesto. Si el tamaño de todos los elementos flexibles a lo largo del eje principal es menor que el principal, pueden crecer hasta llenar ese espacio.
Entonces, de forma predeterminada, los elementos flexibles:
- no crezcas;
- si de otro modo desbordarían al padre, se les permite encogerse;
- su ancho actúa como
max-content
.
En conjunto, la flex
abreviatura predeterminada es:
.selector { flex: 0 1 auto;}
Lo divertido de la flex
taquigrafía es que puedes omitir valores. Entonces, cuando declaramos flex: 1
, establecemos el primer valor, flex-grow
en 1
, lo que básicamente activa flex-grow
.
Lo extraño aquí es lo que sucede con los valores que omites . Se esperaría que mantuvieran sus valores predeterminados, pero no es así. Sin embargo, antes de llegar a lo que sucede, profundicemos en lo flex-grow
que sucede.
Como hemos visto, flex-shrink
permite que los elementos se reduzcan si sus tamaños base suman un valor calculado que es mayor que el ancho disponible del contenedor principal. flex-grow
es lo opuesto. Si el total general de los tamaños base de los elementos es menor que el valor del ancho del contenedor principal, crecerán hasta llenar el espacio disponible.
Si tomamos ese ejemplo súper básico donde tenemos tres pequeños divs uno al lado del otro y agregamos flex-grow: 1
, crecerán hasta llenar ese espacio sobrante.
Pero si tenemos tres divs con anchos desiguales, como aquellos con los que comenzamos, agregarlos flex-grow
no hará nada en absoluto. No crecerán porque ya están ocupando todo el espacio disponible; ¡tanto espacio, de hecho, que flex-shrink
es necesario encogerlos para que quepan!
Pero, como me han señalado algunas personas, la configuración flex: 1
puede funcionar para crear columnas iguales. Bueno, más o menos, ¡como vimos arriba! Sin embargo, en situaciones simples funciona, como puede ver a continuación.
Cuando lo declaramos flex: 1
, funciona porque no solo establece el valor flex-grow
en 1
, sino que también cambia el valor flex-basis
!
.selector { flex: 1; /* flex-grow: 1; */ /* flex-shrink: 1; */ /* flex-basis: 0%; Wait what? */}
Sí, la configuración flex: 1
establece el flex-basis
valor en 0%
. Esto sobrescribe el tamaño intrínseco que teníamos antes y que se comportaba como max-content
. ¡Todos nuestros elementos flexibles ahora quieren tener un tamaño base de 0
!
Entonces, sus tamaños de base son 0
ahora, pero debido a flex-grow
, todos pueden crecer para llenar el espacio vacío. Y realmente, en este caso, flex-shrink
ya no está haciendo nada, ya que todos los elementos flexibles ahora tienen un ancho de 0
y están creciendo para llenar el espacio disponible.
Al igual que en el ejemplo anterior de reducción, tomamos el espacio disponible y dejamos que todos los elementos flexibles crezcan al mismo ritmo. Dado que todos tienen un ancho de base de 0
, crecer a la misma velocidad significa que el espacio disponible se divide equitativamente entre ellos y ¡todos tienen el mismo tamaño final!
Excepto que, como vimos, ese no es siempre el caso...
La razón de esto es que, cuando flexbox hace todo esto y distribuye el espacio, ya sea reduciendo o haciendo crecer un elemento flexible, está mirando el tamaño del contenido del elemento. Si recuerdas el modelo de caja, tenemos el tamaño del contenido en sí, luego padding
, border
y margin
fuera de eso.
Y no, no lo olvidé * { box-sizing: border-box; }
.
Esta es una de esas extrañas peculiaridades de CSS pero tiene sentido. Si el navegador observara el ancho de esos elementos e incluyera su padding
y borders
en los cálculos, ¿cómo podría reducirlos? O padding
también tendrían que encogerse o las cosas van a desbordar el espacio. Ambas situaciones son terribles , por lo que en lugar de mirar los box-size
elementos al calcular su tamaño base, ¡solo mira el cuadro de contenido!
Por lo tanto, la configuración flex: 1
causa un problema en los casos en que tiene border
s o padding
en algunos de sus elementos. Ahora tengo tres elementos que tienen un tamaño de contenido de 0
, pero el del medio tiene relleno. Si no tuviéramos ese relleno, tendríamos los mismos cálculos que cuando analizamos cómo flex-shrink
funciona.
Un padre que es 600px
y tres elementos flexibles con un ancho de 0px
. Todos tienen una flex-grow: 1
forma, por lo que crecen al mismo ritmo y cada uno termina 200px
ancho. Pero el acolchado lo estropea todo . En cambio, termino con tres divs con un tamaño de contenido de 0
, pero el del medio tiene padding: 1rem
. Eso significa que tiene un tamaño de contenido de 0
, además 32px
de relleno también.
Tenemos 600 - 32 = 568px
que dividir en partes iguales, en lugar de 600px
. Todos los divs quieren crecer al mismo ritmo, entonces 568 / 3 = 189.3333px
.
¡Y eso es lo que pasa!
Pero... recuerda, ¡ese es el tamaño del contenido, no el ancho total! Eso nos deja con dos divs con un ancho de 189.333px
y otro con un cual de 189.333px + 32 = 221.333px
. ¡Esa es una diferencia bastante sustancial!
Método 2:flex-basis: 100%
Siempre he manejado esto así:
.flex-parent { display: flex;}.flex-parent * { flex-basis: 100%;}
Pensé que esto funcionó durante mucho tiempo. En realidad, se suponía que esta sería mi solución final después de demostrar que flex: 1
no funciona. Pero mientras escribía este artículo, me di cuenta de que también cae en el mismo problema, pero es mucho menos obvio. Lo suficiente como para que no lo notara con mis ojos.
Todos los elementos intentan tener 100%
un ancho, lo que significa que todos quieren coincidir con el ancho del padre, que, nuevamente, es 600px
(y en circunstancias normales, suele ser mucho más grande, lo que reduce aún más la diferencia perceptible).
La cuestión es que 100%
incluye el relleno en los valores calculados (debido a * { box-size: border-box; }
, que por el bien de este artículo, supongo que todos están usando). Entonces, los divs externos terminan con un tamaño de contenido de 600px
, mientras que el div del medio termina con un tamaño de contenido de 600 - 32 = 568px
.
Cuando el navegador está averiguando cómo dividir uniformemente el espacio, no está mirando cómo aplastar uniformemente 1800px
un 600px
espacio, sino más bien cómo aplastar 1768px
. Además, como vimos antes, los artículos flexibles no se encogen en la misma cantidad, ¡sino al mismo ritmo! Entonces, el elemento con relleno se encoge un poco menos en total que los demás.
Esto da como resultado que .card
tenga un ancho final de 214.483px
mientras que los demás registran en 192.75px
. Nuevamente, esto conduce a valores de ancho desiguales, aunque la diferencia es menor de lo que vimos con la flex: 1
solución.
Por qué CSS Grid es la mejor opción aquí
Si bien todo esto es un poco frustrante (e incluso un poco confuso), todo sucede por una buena razón. Si los márgenes, el relleno o los bordes cambiaran de tamaño cuando interviene la flexión, sería una pesadilla.
Y tal vez esto signifique que CSS Grid podría ser una mejor solución para este patrón de diseño tan común.
Durante mucho tiempo pensé que flexbox era más fácil de aprender y comenzar a usar que grid, pero grid te brinda un mayor control a largo plazo, pero es mucho más difícil de entender. Sin embargo, cambié de opinión al respecto recientemente y creo que la cuadrícula no solo nos brinda un mejor control en este tipo de situaciones, sino que también es más intuitiva .
Normalmente, cuando usamos grid, declaramos explícitamente nuestras columnas usando grid-template-columns
. Aunque no tenemos que hacer eso. Podemos hacer que se comporte de manera muy similar a lo que hace flexbox usando grid-auto-flow: column
.
.grid-container { display: grid; grid-auto-flow: column;}
Así, terminamos con el mismo tipo de comportamiento que arrojar display: flex
allí. Por ejemplo flex
, las columnas pueden estar potencialmente desequilibradas, pero la ventaja de la cuadrícula es que el padre tiene control total sobre todo . Entonces, en lugar de que el contenido de los elementos tenga un impacto como lo haría en flexbox, solo necesitamos una línea más de código y podemos resolver el problema:
.grid-container { display: grid; grid-auto-flow: column; grid-auto-columns: 1fr;}
¡Me encanta que todo esto esté en el selector de padres y que no tengamos que seleccionar a los hijos para ayudar a obtener el diseño que buscamos!
Lo interesante aquí es cómo fr
funcionan las unidades. Literalmente se denominan "unidades flexibles" en la especificación y funcionan igual que Flexbox para dividir el espacio. La gran diferencia: miran las otras pistas para determinar cuánto espacio tienen , sin prestar atención al contenido dentro de esas pistas.
Esa es la verdadera magia aquí. Al declarar grid-auto-columns: 1fr
, en esencia estamos diciendo: "de forma predeterminada, todas mis columnas deben tener el mismo ancho", ¡que es lo que hemos buscado desde el principio!
Pero ¿qué pasa con las pantallas pequeñas?
Lo que me encanta de este enfoque es que podemos mantenerlo súper simple:
.grid-container { display: grid; gap: 1em;}@media (min-width: 35em) { grid-auto-flow: column; grid-auto-columns: 1fr;}
Y así, funciona perfectamente. Y al declarar display: grid
desde el principio, podemos incluir para gap
mantener el mismo espacio, ya sea que los elementos secundarios sean filas o columnas.
También encuentro que esto es mucho más intuitivo que cambiar flex-direction
dentro de una consulta de medios para obtener un resultado similar. Por mucho que me guste flexbox (y realmente sigo pensando que tiene excelentes casos de uso), el hecho de que la declaración flex-direction: column
cree filas, y viceversa, es un poco contrario a la intuición a primera vista.
Y, por supuesto, si prefieres continuar sin consultas de medios, no hay nada que te impida llevar esto al siguiente nivel con la ayuda de auto-fit
, que sería similar a configurar algo con flex-wrap
(aunque no exactamente lo mismo):
.grid-container { display: grid; gap: 1em; grid-template-columns: repeat(auto-fit, minmax(10em, 25em));}
Haciendo que funcione con flexbox
Me doy cuenta de que podemos solucionar esto con flexbox anidando el elemento con el padding
. Hemos hecho esto desde que comenzamos a hacer diseños usando flotadores, y Bootstrap realmente logró implementar este tipo de patrón de diseño.
div div div div... /div div div.../div /div div ... /div /div/div
Y no hay nada malo en ello. ¡Funciona! Pero los flotadores también funcionan y hemos dejado de usarlos para diseños a medida que se han lanzado mejores soluciones en los últimos años.
Una de las razones por las que me encanta la cuadrícula es porque podemos simplificar bastante nuestro marcado. Así como abandonamos los flotadores por una mejor solución, al menos me gustaría que la gente mantuviera la mente abierta a que tal vez, solo tal vez, la cuadrícula podría ser una solución mejor y más intuitiva para este tipo de patrón de diseño.
Flexbox todavía tiene su lugar, por supuesto.
Todavía me encanta flexbox, pero creo que su poder real proviene de momentos en los que queremos confiar en el ancho intrínseco de los elementos flexibles , como con las navegaciones o grupos de elementos, como botones u otros elementos de ancho variable que desee. ir uno al lado del otro.
En esas situaciones, ese comportamiento es una ventaja que hace que cosas como el espaciado uniforme entre elementos desiguales sean muy fáciles. Flexbox es maravilloso y no tengo planes de dejar de usarlo.
Sin embargo, en otras situaciones, cuando te encuentras luchando con cómo intenta funcionar flexbox, tal vez puedas recurrir a la cuadrícula, incluso si no es una cuadrícula “2d” típica para la que te dicen que deberías usarla.
La gente suele decirme que les cuesta entender la cuadrícula porque es demasiado complicada y, si bien puede serlo, como vimos aquí, no tiene por qué serlo.
Deja un comentario