El switcher: cambiar de fila a columna con un umbral
Cómo hacer que un grupo de elementos pase de una fila a una columna en un ancho concreto usando solo aritmética de flex-basis, y por qué el calc con multiplicador gigante funciona.
El sidebar de la lección anterior apila dos zonas asimétricas. El switcher resuelve el caso hermano y más común: un grupo de elementos iguales que deben ir en fila mientras haya sitio y en columna cuando no lo haya, con un umbral que tú eliges y que no depende del viewport. La implementación es una de las piezas de CSS más ingeniosas que existen y descansa entera sobre una regla de la especificación acerca de los valores fuera de rango en calc().
- Implementar el switcher con
flex-basiscalculado y explicar cada término delcalc(). - Justificar por qué un resultado negativo se convierte en cero y no en un error.
- Elegir el umbral en función del contenido y no de un tamaño de dispositivo.
- Añadir la salvaguarda que evita filas con demasiados elementos.
El patrón
.switcher {
--umbral: 30rem;
display: flex;
flex-wrap: wrap;
gap: 1rem;
}
.switcher > * {
flex-grow: 1;
flex-basis: calc((var(--umbral) - 100%) * 999);
}
Con el contenedor más ancho que 30rem, los hijos van en una fila repartiéndose el espacio a partes iguales. Con el contenedor más estrecho, cada hijo salta a su propia línea y ocupa el ancho completo. No hay ninguna condición escrita en ninguna parte: hay una fórmula.
La fórmula y el umbral
Cómo funciona la fórmula
100% dentro de flex-basis se resuelve contra el tamaño en línea del contenedor. Así que var(--umbral) - 100% es la diferencia entre el umbral y el ancho real, y su signo es toda la lógica.
Cuando el contenedor mide menos que el umbral, la resta es positiva. Multiplicada por 999 da un número enorme —cientos de rem—, y ese es el tamaño base que cada hijo pide. Como no caben ni dos, flex-wrap los reparte a línea por hijo, y en cada línea el flex-shrink por defecto los devuelve al ancho del contenedor. Resultado: una columna.
Cuando el contenedor mide más que el umbral, la resta es negativa y el producto también. Aquí entra la regla que hace posible el patrón: flex-basis no acepta longitudes negativas, y la especificación de valores y unidades dice que un resultado de calc() fuera del rango permitido por la propiedad se acota al valor válido más cercano en tiempo de cálculo, en lugar de invalidar la declaración. El valor se convierte en cero. Con base cero y flex-grow: 1, los hijos se reparten todo el ancho a partes iguales en una sola fila.
El 999 no tiene nada de mágico: solo tiene que ser lo bastante grande como para que la diferencia positiva más pequeña posible —un contenedor un píxel por debajo del umbral— produzca una base que ningún contenedor pueda albergar dos veces. Cualquier número de tres cifras sirve.
La diferencia parece sutil y no lo es: --umbral: 30rem mide el contenedor, no la ventana. El mismo componente colocado en una barra lateral de 22rem se comportará como columna aunque el monitor tenga 3000px de ancho, que es justo lo que una media query no puede darte. Además, al estar expresado en rem, el umbral se mueve con el tamaño de letra del usuario: quien navegue con la raíz a 24px obtendrá el cambio a columna antes, que es exactamente lo correcto.
Elegir el umbral
El umbral debe salir de una propiedad del contenido, no de un catálogo de pantallas. Las dos preguntas que lo determinan casi siempre son cuántos caracteres necesita el texto más corto para no quedar ridículo y a partir de qué ancho una imagen deja de leerse.
/* dos paneles de texto: por debajo de 45ch por panel, mejor apilados */
.dos-paneles { --umbral: 90ch; }
/* formulario en linea: el campo necesita 24rem para ser usable */
.campo-y-boton { --umbral: 32rem; }
Usar ch en el umbral es especialmente coherente: ch es una unidad relativa a la fuente, con lo que el punto de cambio se ajusta si el componente hereda una tipografía más grande. El umbral deja de ser un número sobre la pantalla y pasa a ser una afirmación sobre la legibilidad.
La salvaguarda de los muchos elementos
El switcher reparte a partes iguales todos los hijos que quepan en una línea. Con dos o tres da resultados buenos; con siete produce columnas de 100px que no sirven para nada. La solución idiomática usa selectores estructurales para detectar el exceso y forzar el ancho completo:
/* si hay 5 hijos o mas, que todos ocupen la linea entera */
.switcher > :nth-last-child(n + 5),
.switcher > :nth-last-child(n + 5) ~ * {
flex-basis: 100%;
}
El selector merece una lectura despacio. :nth-last-child(n + 5) selecciona los elementos que tienen al menos cuatro hermanos después, es decir, todos menos los cuatro últimos; solo hay alguno si hay cinco o más en total. El segundo selector, con el combinador de hermanos generales, recoge los cuatro que faltaban. Entre los dos cubren la lista entera si y solo si la lista tiene cinco o más elementos. Es contar sin contar, y es el mismo mecanismo que hace posible el CSS de cantidad.
Cuándo el switcher es peor que el patrón RAM
El switcher fuerza a que todos los elementos de una línea midan lo mismo y a que el cambio sea binario: fila entera o columna entera. Si lo que tienes es una colección de longitud variable donde lo natural es que pase de cuatro a tres a dos columnas, el patrón RAM es mejor herramienta y más simple.
La regla práctica: switcher para grupos pequeños y de tamaño conocido —dos paneles, tres pasos, un campo con su botón—; RAM para colecciones. Y si necesitas el reparto equitativo de RAM pero con un cambio brusco en un umbral concreto, la combinación de los dos también es legítima.
Este patrón se suele contar como una curiosidad y merece más respeto, porque enseña algo estructural sobre CSS: en un lenguaje declarativo puro, una condición se puede codificar como una función continua con una discontinuidad práctica. La resta cambia de signo en el umbral, la multiplicación amplifica esa diferencia hasta un valor que fuerza un comportamiento cualitativamente distinto, y el acotado a cero convierte todo el rango negativo en un único caso. Es la misma técnica que usarías en un shader para evitar una rama, y por las mismas razones: no hay estado, no hay dos caminos que puedan divergir, y no hay ninguna condición almacenada que pueda quedarse desactualizada respecto a los valores de los que depende. Interiorizar esto cambia la forma de leer CSS: cuando veas min(), max(), clamp() o un calc() con multiplicador, no estás viendo aritmética adornada, estás viendo lógica compilada a expresiones de valores. Y cuando el navegador entiende la lógica en lugar de recibir el resultado, puede reevaluarla en circunstancias que tú nunca previste, que es la definición operativa de un layout que envejece bien.
- Implementa el switcher y comprueba con el inspector qué valor tiene
flex-basisa cada lado del umbral. - Cambia el 999 por 2 y explica por qué deja de funcionar.
- Sustituye el umbral en
rempor uno ench, aumenta el tamaño de letra de la página y observa cómo se mueve el punto de cambio. - Añade siete hijos y aplica la salvaguarda con
:nth-last-child; verifica que con cuatro no se activa. - Mete el mismo switcher dentro de una barra lateral estrecha y confirma que se comporta como columna con la ventana maximizada.