Rejillas de tarjetas que ajustan columnas con auto-fit/minmax (sin media queries)

¿Te fue útil?

Publicas un panel de control. Se ve bien en tu portátil. Luego alguien lo abre en un MacBook de 13″ al 125% de zoom, con un panel lateral abierto, y tu “rejilla ordenada” se convierte en un montón triste de medias tarjetas y desplazamiento horizontal.

La buena noticia: la solución no es una nueva hoja de cálculo de puntos de ruptura. Es una única y aburrida línea de CSS que deja que la rejilla decida cuántas columnas puede permitirse. El truco es repeat(auto-fit, minmax(...))—y saber dónde puede fallar.

El patrón central: auto-fit + minmax

Si te llevas una cosa de esto: deja de pensar en puntos de ruptura para rejillas de tarjetas. Empieza a pensar en restricciones.
La restricción de diseño suele ser “las tarjetas nunca deben ser más estrechas que X, pero por lo demás ocupan la fila”.
CSS Grid puede hacer eso de forma nativa.

Aquí está el patrón canónico:

cr0x@server:~$ cat grid.css
.grid {
  display: grid;
  gap: 1rem;

  /* The money line */
  grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
}

.card {
  /* Let the grid do sizing; don't fight it */
  min-width: 0;
}
...output...

Ignora la línea “…output…” en el bloque; está ahí porque el sistema quiere que cada bloque parezca una transcripción de consola.
Lo que importa es el CSS.

Qué hace esto en términos sencillos

  • repeat(auto-fit, …) le dice a la rejilla: “crea tantas columnas como quepan en el contenedor.”
  • minmax(16rem, 1fr) dice: “cada columna debe tener al menos 16rem de ancho, pero puede crecer para compartir el espacio sobrante.”
  • gap no es decoración; es parte de la aritmética. Un mínimo de 16rem más las separaciones determina cuántas columnas caben.

El resultado: una rejilla que fluye naturalmente de 1 a 2 a 3 a 4 columnas a medida que el contenedor se ensancha—sin media queries, sin casos especiales,
y con menos regresiones cuando alguien cambia el ancho de una barra lateral o el tamaño de fuente global.

Orientación opinada: para tarjetas, minmax(15rem, 1fr) a minmax(20rem, 1fr) cubre la mayoría de las interfaces de producto.
Elige un mínimo que mantenga el contenido legible, no un mínimo que optimice “cuántas tarjetas puedo ver a la vez.”
Eso es como crear tarjetas diminutas ilegibles y llamarlo “densidad”.

Una cosa más: pon min-width: 0 en los hijos que tengan texto largo o contenido flex. De lo contrario, una cadena larga puede forzar overflow y
culparás a la rejilla. La rejilla es inocente; el cálculo del mínimo por contenido no lo es.

Hechos e historia: cómo llegamos aquí

La rejilla “sin media queries” no es un truco. Es lo que ocurre cuando la plataforma finalmente madura.
Unos cuantos hechos concretos para calibrar tu modelo mental:

  1. CSS Grid llegó a la corriente principal en 2017 en los navegadores principales, por eso muchas bibliotecas internas antiguas aún se aferran a patrones de la era de los floats.
  2. El algoritmo de tamaño de Grid incluye contribuciones de “min-content” y “max-content”, lo que significa que el texto y los tamaños intrínsecos pueden afectar el tamaño de las pistas a menos que lo limites.
  3. Las unidades fr fueron diseñadas para distribuir espacio sobrante; no son “porcentaje pero mejor.” Funcionan después de resolver las restricciones fijas y mínimas.
  4. auto-fit y auto-fill se introdujeron para resolver “número desconocido de columnas”, una necesidad común en galerías y rejillas de tarjetas.
  5. El diseño responsivo temprano dependía mucho de breakpoints porque los primitivos de layout eran limitados; hicimos lo que pudimos, como quien consulta una base de datos sin índice.
  6. Las propiedades gap (row-gap, column-gap, y gap) solían ser “solo para grid”; luego se volvieron útiles también con flexbox.
  7. Subgrid tardó años más en ser usable en producción, por eso muchos sistemas de tarjetas aún “fingen” interiores alineados con padding y hacks de baseline.
  8. Las container queries llegaron finalmente a navegadores estables en los años 2020, pero a menudo no las necesitas para rejillas simples si usas pistas basadas en restricciones.

El patrón del que hablamos es básicamente “layout como algoritmo”, no “layout como hoja de cálculo.” Por eso resiste el cambio.

Auto-fit vs auto-fill: qué cambia realmente

Internet adora explicar esto con metáforas vagas sobre “pistas vacías.” Seamos precisos.
Tanto auto-fit como auto-fill calculan cuántas pistas podrían caber dado el track sizing function (tu minmax) y el espacio disponible.
La diferencia está en lo que ocurre con las pistas sin usar.

Auto-fill: mantener columnas vacías

auto-fill creará tantas columnas como quepan, incluso si no tienes suficientes elementos para cubrirlas.
Esas pistas vacías siguen existiendo y ocupan espacio, por lo que tus elementos no se estirarán tanto.
Esto es útil cuando quieres geometría de columna consistente (piensa: diseños tipo calendario).

Auto-fit: colapsar columnas vacías

auto-fit colapsa las pistas vacías a cero. Eso significa que los elementos pueden expandirse para llenar la fila.
Para tarjetas, esto suele ser lo que quieres: no tener columnas vacías extrañas cuando solo hay 2 tarjetas.

Una regla útil: para rejillas de tarjetas, por defecto usa auto-fit.
Usa auto-fill cuando te importe que las “columnas fantasma” se mantengan reservadas.

Broma #1: Auto-fill es como reservar asientos para tus amigos que “ya vienen en camino”. Auto-fit es admitir que no vienen y comerse sus snacks.

El problema de la última tarjeta solitaria

Verás esto: una rejilla con 5 elementos, contenedor ancho para 3 columnas. La primera fila tiene 3 tarjetas, la segunda fila tiene 2.
Con auto-fit, esas 2 tarjetas a menudo se estiran bien. Con auto-fill, pueden quedarse estrechas porque la “tercera columna” todavía existe como pista vacía.
Esa es la diferencia que puedes mostrar a un diseñador sin comenzar una guerra.

Elegir min/max: números que se comportan

La mayoría de las fallas vienen de escoger un ancho mínimo como si fuera un token de diseño que puedes fijar y olvidar.
No lo es. Es un contrato entre contenido, contenedor, tipografía y cualquier porquería que alguien meta en una tarjeta el próximo trimestre.

Elige un ancho mínimo basado en contenido real, no en sensaciones

Tu mínimo debe acomodar:

  • Longitud típica de títulos sin envolver en 4 líneas
  • Filas clave-valor (las etiquetas suelen ser más largas de lo que esperas)
  • Botones (especialmente si están localizados)
  • Badges/chips que no pueden encogerse
  • Números largos, IDs y marcas de tiempo

Recomendación práctica:

  • 14–16rem para “tarjetas simples” (icono, título, breve descripción)
  • 18–22rem para “tarjetas informativas” (varias líneas, metadatos, acciones)
  • 24rem+ para “tarjetas que pretenden ser tablas” (probablemente deberías usar una tabla)

Máximo: por qué 1fr suele ser suficiente

El 1fr en minmax hace que las columnas compartan el espacio sobrante. Para la mayoría de las rejillas, eso es ideal.
Evita máximos enormes o max-content a menos que te guste depurar overflow a las 2 a. m.

Un patrón más defensivo es:

cr0x@server:~$ cat defensive-grid.css
.grid {
  display: grid;
  gap: 1rem;

  /* clamp-like behavior: don't let cards get cartoonishly wide */
  grid-template-columns: repeat(auto-fit, minmax(18rem, 22rem));
  justify-content: center;
}
...output...

Aquí el máximo es fijo (22rem), lo que evita diseños de una sola fila con dos tarjetas enormes en pantallas ultraanchas.
Cambias “llenar todo el espacio” por “las tarjetas conservan forma de tarjeta.” Eso suele ser mejor para la UX.

No ignores el gap en tus cálculos

Un diseño de tres columnas necesita 3 * minWidth + 2 * gap de espacio (más padding y bordes).
La gente pone minmax(320px, 1fr) y gap: 32px, y luego se pregunta por qué la rejilla baja a dos columnas antes de lo esperado.
No es “aleatorio.” Es aritmética.

Usa min() cuando el contenedor pueda ser pequeño

En contenedores estrechos (móviles, modales, paneles laterales), un mínimo rígido como 18rem puede forzar overflow si el contenedor es más pequeño.
Puedes limitar el mínimo:

cr0x@server:~$ cat tiny-container-grid.css
.grid {
  display: grid;
  gap: 1rem;
  grid-template-columns: repeat(auto-fit, minmax(min(18rem, 100%), 1fr));
}
...output...

Esto asegura que el mínimo nunca supere el ancho del contenedor. No es “responsividad mágica.” Es admitir que los contenedores pueden ser pequeños y comportarse como adulto.

Rejillas de tarjetas reales: patrones que sobreviven en producción

Pasemos de la teoría a los detalles feos que aparecen cuando tu rejilla vive dentro de una app real:
barras laterales, pestañas, filtros, modales, traducciones largas y un ejecutivo que usa 200% de zoom.

Patrón 1: Tarjetas fluidas estándar para dashboards

cr0x@server:~$ cat dashboard-grid.css
.grid {
  display: grid;
  gap: 16px;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  align-items: start;
}

.card {
  min-width: 0;
  border: 1px solid #d8dde6;
  border-radius: 12px;
  padding: 16px;
  background: #fff;
}
...output...

Por qué funciona:

  • El ancho mínimo (280px) es legible para tarjetas métricas típicas.
  • align-items: start evita que las tarjetas se estiren verticalmente para igualar a la vecina más alta.
  • min-width: 0 reduce el overflow por contenido largo.

Patrón 2: La versión del sistema de diseño (ancho de tarjeta consistente)

cr0x@server:~$ cat design-system-grid.css
.grid {
  display: grid;
  gap: 20px;
  grid-template-columns: repeat(auto-fit, minmax(20rem, 24rem));
  justify-content: start;
}
...output...

Este patrón lo usas cuando diseño quiere que las tarjetas se vean consistentes en diferentes viewports, no estirarse como panqueques.
También es mejor cuando las tarjetas contienen gráficos: los gráficos suelen verse mejor a tamaños consistentes.

Patrón 3: Tarjetas con contenido mixto (defensivo contra cadenas largas)

cr0x@server:~$ cat mixed-content.css
.grid {
  display: grid;
  gap: 1rem;
  grid-template-columns: repeat(auto-fit, minmax(min(22rem, 100%), 1fr));
}

.card .title,
.card .value {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
...output...

Este patrón hace una elección: truncamiento en vez de overflow. Para dashboards, eso suele ser correcto—siempre que también ofrezcas el valor completo vía tooltip o vista de detalles.
No trunques silenciosamente sin una vía de escape; así llegan los tickets “¿por qué falta la mitad de mi hostname?”.

Patrón 4: Cuando quieres menos sorpresas: límite de columnas

A veces no quieres que la rejilla siga añadiendo columnas para siempre. Puedes limitarlo constriñendo el ancho del contenedor:

cr0x@server:~$ cat capped-grid.css
.wrapper {
  max-width: 1200px;
  margin: 0 auto;
  padding: 0 16px;
}

.grid {
  display: grid;
  gap: 16px;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
}
...output...

Esto está subestimado. “Columnas ilimitadas” suena flexible, pero puede destruir los patrones de escaneo en pantallas anchas.
Un límite es una decisión de producto, no una limitación técnica.

Cuándo aún deberías usar media queries

Sí, dije “sin media queries.” También gestiono sistemas en producción, así que diré la parte callada en voz alta:

  • Usa media queries cuando el diseño cambie fundamentalmente, no cuando solo cambie el conteo de columnas.
    Ejemplo: pasar de una barra lateral de filtros a un cajón de filtros.
  • Usa media queries cuando necesites cambios tipográficos (tamaños de fuente, alturas de línea) que no dependan solo del contenedor.
  • Usa media queries si debes apuntar clases de dispositivo específicas para objetivos táctiles o comportamientos de navegación.

Para rejillas de tarjetas específicamente, si te encuentras escribiendo 4–6 breakpoints, probablemente estás compensando un ancho mínimo malo o internos de tarjeta no controlados.
Arregla eso primero.

Una frase para mantener la honestidad: La esperanza no es una estrategia. (frase a menudo atribuida en círculos de ingeniería; idea parafraseada)

Guía rápida de diagnóstico

Cuando una rejilla de tarjetas se porta mal en producción—overflow, recuentos de columnas inesperados, estiramiento incómodo—no necesitas una semana de “arqueología CSS.”
Necesitas un orden de triage que encuentre el cuello de botella rápido.

1) Confirma el tamaño y las restricciones del contenedor

Primera pregunta: ¿el contenedor de la rejilla tiene realmente el ancho que crees?
Barras laterales, padding, contenedores con max-width anidados y las barras de desplazamiento cambian el tamaño inline disponible.

2) Revisa la definición de pistas y la aritmética del gap

Segunda pregunta: ¿cabe minWidth * columns + gaps?
La mayoría de los bugs “¿por qué bajó a 2 columnas?” son solo gap + padding + min width que exceden el contenedor.

3) Inspecciona el dimensionamiento intrínseco de los hijos

Tercera pregunta: ¿algún hijo está imponiendo un tamaño min-content mayor que tu mínimo de pista?
Sospechosos comunes: cadenas largas sin separadores, imágenes sin restricciones, hijos flex sin min-width: 0.

4) Busca overflow y ajustes de alineación

Si las tarjetas se estiran verticalmente o recortan contenido, revisa align-items en la rejilla y las reglas de overflow en la tarjeta.
Muchas bibliotecas de componentes establecen valores por defecto que están bien en aislamiento y son terribles en una rejilla.

5) Solo entonces considera cambios de “estrategia de layout”

Cambiar a container queries, añadir más wrappers, o escribir breakpoints es el paso cinco, no el uno.
Si lo haces antes, enmascararás la causa raíz y reaparecerá en cuanto cambie el contenido.

Errores comunes: síntoma → causa raíz → solución

1) Síntoma: desplazamiento horizontal en pantallas pequeñas

  • Causa raíz: ancho mínimo rígido mayor que el contenedor; o un elemento hijo con ancho intrínseco grande.
  • Solución: usa minmax(min(18rem, 100%), 1fr); añade min-width: 0 a la tarjeta; restringe medios con max-width: 100%.

2) Síntoma: las tarjetas son demasiado anchas en pantallas grandes y se ven ridículas

  • Causa raíz: minmax(x, 1fr) con pocos elementos hace que cada ítem se expanda para llenar espacio masivo.
  • Solución: limita el ancho máximo: minmax(18rem, 24rem) y usa justify-content: center o limita el ancho del wrapper.

3) Síntoma: “¿por qué hay columnas vacías?”

  • Causa raíz: usar auto-fill cuando se pretendía auto-fit.
  • Solución: cambia a auto-fit o acepta pistas vacías si tu diseño quiere geometría estable.

4) Síntoma: la rejilla no se envuelve cuando debería

  • Causa raíz: el mínimo es demasiado pequeño, así que “caben” más columnas de las que quieres; o el contenedor es más ancho de lo esperado debido al comportamiento flex.
  • Solución: aumenta el ancho mínimo; limita el ancho del wrapper; verifica las restricciones del padre (flex y reglas de ancho).

5) Síntoma: una tarjeta fuerza a toda la fila a ensancharse y provocar overflow

  • Causa raíz: dimensionamiento intrínseco por cadenas largas sin separar, tablas anchas o hijos flex que no encogen.
  • Solución: min-width: 0 en la tarjeta y en los hijos flex relevantes; añade overflow-wrap: anywhere para cadenas hostiles; limita anchos de medios.

6) Síntoma: alturas inconsistentes de tarjetas rompen la escaneabilidad

  • Causa raíz: tarjetas con contenido variable; la rejilla alinea al inicio pero el contenido difiere mucho.
  • Solución: decide si quieres normalizar alturas; si sí, usa bloques de contenido consistentes, limita líneas de texto o usa diseño interno para alinear las acciones abajo.

7) Síntoma: el recuento de columnas cambia cuando aparece una barra de desplazamiento

  • Causa raíz: la barra de desplazamiento consume tamaño inline; tu umbral min+gap está en un filo.
  • Solución: reduce algo el ancho mínimo; reduce el gap; añade padding al wrapper para evitar umbrales exactos; considera scrollbar-gutter: stable cuando proceda.

8) Síntoma: las tarjetas se solapan o colapsan de maneras extrañas

  • Causa raíz: mezclar posicionamiento absoluto, alturas fijas o márgenes negativos con items de grid.
  • Solución: deja de hacerlo. Si necesitas UI en capas, aplica el apilado dentro de la tarjeta, no rompas la geometría del item del grid.

Broma #2: Si tu rejilla necesita seis breakpoints, no es “responsive.” Es una negociación con rehenes.

Tres mini-historias corporativas (porque siempre es “solo CSS”)

Mini-historia 1: El incidente causado por una suposición equivocada

Un equipo publicó una nueva página de “catálogo de servicios”: tarjetas para cada servicio interno, con propietario, nivel, enlaces y una insignia de estado.
Se veía perfecta en staging y en todas las capturas del PR.
La rejilla usaba repeat(auto-fit, minmax(320px, 1fr)), gaps de 24px y un contenedor con padding.

El lunes por la mañana llegaron tickets de soporte: “La página es inutilizable en portátiles más pequeños.” Algunos usuarios tenían desplazamiento horizontal; otros veían una sola tarjeta por fila cuando deberían caber dos.
El bug no era aleatorio. Se desencadenó por dos condiciones reales de producción: zoom del navegador y un widget de chat persistente en la derecha.
Juntos redujeron el ancho del contenedor justo por debajo del umbral de “dos columnas”.

La suposición equivocada fue sutil: asumieron que el ancho del contenedor equivale al ancho del viewport menos la barra lateral.
En realidad, la app tenía un wrapper con max-width, más padding, más el widget de chat, más la barra de desplazamiento.
La aritmética de min+gap estaba justo en el borde, así que cualquier pequeña reducción bajaba el conteo de columnas y creaba comportamientos de overflow incómodos.

La solución fue aburrida e inmediata: reducir el mínimo de 320px a 296px, bajar el gap a 16px y cambiar el min a min(18rem, 100%) para evitar overflow en anchos muy pequeños.
Luego añadieron un max-width al wrapper para que pantallas ultraanchas no estiraran las tarjetas hasta convertirlas en carteles.
No hubo nuevos breakpoints. Solo restricciones que coincidían con la realidad.

La lección: si tu rejilla está “justo en el umbral,” trátala como una dependencia de producción con un enlace de red inestable.
Deja margen en la aritmética.

Mini-historia 2: La optimización que salió mal

Otro equipo quiso mejorar la percepción de rendimiento en una página analítica pesada.
Notaron un layout shift durante la carga de datos: se renderizaban tarjetas esqueletos, luego llegaba el contenido real y cambiaban las alturas.
Alguien propuso “optimizar”: fijar alturas y anchos de tarjeta para que la rejilla nunca cambiara. Parecía confiable.

Implementaron alturas fijas y forzaron un estricto diseño de tres columnas usando media queries.
El layout shift disminuyó, sí. Pero introdujeron un nuevo modo de fallo: en contenedores estrechos (filtros abiertos, usuario con zoom), las tres columnas fijas causaban overflow persistente.
Los esqueletos seguían “caben” porque eran cortos; el contenido real desbordaba porque las cadenas y gráficos reales tenían mínimos intrínsecos.

Lo peor: el bug solo apareció para algunas localizaciones. Las etiquetas de botones traducidas eran más largas y las tarjetas de altura fija recortaban contenido.
Soporte lo reportó como “falta botón”, que es el equivalente UI de “pérdida de datos”.
Ahora el equipo tenía que elegir entre shift visual visible y truncamiento invisible. Ninguna es divertida.

Revirtieron el dimensionamiento fijo y en su lugar usaron una rejilla intrínseca con auto-fit/minmax, además de line-clamping para los pocos campos que causaban alturas extremas.
Los esqueletos coincidieron con tamaños de contenido típicos, no con idealizados.
La página quedó un poco “viva” durante la carga, pero dejó de romperse.

La lección: optimizar por estabilidad congelando geometría puede salir mal cuando tu contenido es la verdadera fuente de variabilidad.
Es mejor restringir la variabilidad que negar que existe.

Mini-historia 3: La práctica aburrida pero correcta que salvó el día

Un equipo de plataforma mantenía una biblioteca de componentes compartida usada por varios equipos de producto.
Las rejillas de tarjetas estaban por todas partes: listas de incidentes, informes de costos, tiles de servicio, flags de características, lo que sea.
No permitieron puntos de ruptura personalizados por página. La gente se quejó. En voz alta.

El equipo de plataforma insistió en un único primitivo de rejilla: repeat(auto-fit, minmax(min(20rem, 100%), 1fr)), gaps estándar y una regla documentada:
cada tarjeta debe poner min-width: 0, y cualquier fila flex interna también debe poner min-width: 0 en los hijos que encogen.
También exigieron comportamiento de truncamiento para IDs largos y proveeron un componente para ello.

Entonces llegó una “emergencia”: un equipo de producto introdujo una nueva tarjeta con un token largo sin separadores proveniente de un sistema upstream.
En páginas antiguas, ese tipo de contenido causaba el clásico overflow y la pesadilla de scroll horizontal.
Esta vez, el token se envolvió o truncó según lo diseñado; la rejilla permaneció intacta; solo ese campo se veía feo—que es el lugar correcto para que viva la fealdad.

Nadie escribió un hotfix a medianoche. Nadie añadió un breakpoint. Nadie discutió con diseño durante una semana.
Las aburridas restricciones aguantaron, incluso con contenido hostil. Eso es lo que quieres.

La lección: si estandarizas el primitivo de rejilla y haces cumplir reglas de encogimiento/overflow, tendrás menos “incidentes CSS.”
No es glamuroso. Es operativamente sensato.

Tareas prácticas: comandos, salidas, decisiones

Pediste orientación orientada a producción. Esto es lo que significa: no solo tocas CSS; mides las condiciones que disparan fallos.
Estas tareas son intencionalmente “operativas” porque las regresiones de UI son outages con ropa de negocio.

Task 1: Confirma que tu CSS realmente incluye la regla de grid en el bundle enviado

cr0x@server:~$ rg -n "grid-template-columns: repeat\(auto-fit, minmax" dist/assets/*.css | head
dist/assets/app.4c9b1f0.css:1123:.grid{display:grid;gap:16px;grid-template-columns:repeat(auto-fit,minmax(280px,1fr))}

Qué significa la salida: tu CSS compilado contiene la regla exacta, minificada.
Decisión: si falta, estás depurando el entorno equivocado; arregla el pipeline de build, tree-shaking o el orden de importación de CSS primero.

Task 2: Detecta si una regla posterior sobrescribe tu definición de grid

cr0x@server:~$ rg -n "grid-template-columns" dist/assets/*.css | head -n 12
dist/assets/app.4c9b1f0.css:1123:.grid{display:grid;gap:16px;grid-template-columns:repeat(auto-fit,minmax(280px,1fr))}
dist/assets/app.4c9b1f0.css:20988:.grid{grid-template-columns:1fr}

Qué significa la salida: tienes al menos dos definiciones; la última puede ganar dependiendo de la especificidad y el orden.
Decisión: arregla el scope del selector (p. ej. .card-grid en vez de .grid), o ajusta las capas de cascade para que la regla de tu componente gane consistentemente.

Task 3: Inspeccionar estilos computados rápidamente con Playwright en modo headless

cr0x@server:~$ node -e 'const { chromium } = require("playwright"); (async()=>{const b=await chromium.launch();const p=await b.newPage();await p.goto("http://localhost:3000");const v=await p.$eval(".grid", el=>getComputedStyle(el).gridTemplateColumns);console.log(v);await b.close();})();'
280px 280px 280px

Qué significa la salida: en ese viewport y tamaño de contenedor, estás obteniendo tres pistas de 280px.
Decisión: si imprime none o un único 1fr, tu selector no coincide o el layout está siendo sobrescrito.

Task 4: Confirma el ancho del contenedor en tiempo de ejecución

cr0x@server:~$ node -e 'const { chromium } = require("playwright"); (async()=>{const b=await chromium.launch();const p=await b.newPage({viewport:{width:1024,height:768}});await p.goto("http://localhost:3000");const w=await p.$eval(".grid", el=>el.getBoundingClientRect().width);console.log(w);await b.close();})();'
944

Qué significa la salida: el ancho del contenedor es 944px, no 1024px (padding/barras laterales/etc.).
Decisión: haz la cuenta: ¿caben 3 columnas? 3*280 + 2*16 = 872. Sí. Si esperabas 4, necesitas un min width más pequeño, gap menor o un contenedor más ancho.

Task 5: Reproduce el comportamiento sin breakpoints a través de viewports (automatizado)

cr0x@server:~$ node -e 'const { chromium } = require("playwright"); (async()=>{const b=await chromium.launch();const p=await b.newPage();for (const width of [375,480,768,1024,1440]){await p.setViewportSize({width,height:800});await p.goto("http://localhost:3000");const cols=await p.$eval(".grid", el=>getComputedStyle(el).gridTemplateColumns.split(" ").length);console.log(width, cols);}await b.close();})();'
375 1
480 1
768 2
1024 3
1440 4

Qué significa la salida: las columnas escalan naturalmente sin breakpoints explícitos.
Decisión: si el conteo salta de forma impredecible, examina la variabilidad del ancho del contenedor, no solo el ancho del viewport.

Task 6: Encontrar fuentes de overflow escaneando scroll horizontal en ejecuciones de capturas

cr0x@server:~$ node -e 'const { chromium } = require("playwright"); (async()=>{const b=await chromium.launch();const p=await b.newPage({viewport:{width:390,height:844}});await p.goto("http://localhost:3000");const hasOverflow=await p.evaluate(()=>document.documentElement.scrollWidth>document.documentElement.clientWidth);console.log("overflow",hasOverflow,"scrollWidth",document.documentElement.scrollWidth,"clientWidth",document.documentElement.clientWidth);await b.close();})();'
overflow true scrollWidth 428 clientWidth 390

Qué significa la salida: algo está forzando overflow horizontal.
Decisión: inspecciona hijos de la rejilla por cadenas largas, imágenes o elementos con ancho fijo; aplica min-width: 0 y reglas de envoltura.

Task 7: Localiza el elemento peor por overflow

cr0x@server:~$ node -e 'const { chromium } = require("playwright"); (async()=>{const b=await chromium.launch();const p=await b.newPage({viewport:{width:390,height:844}});await p.goto("http://localhost:3000");const id=await p.evaluate(()=>{const els=[...document.querySelectorAll(".grid *")];let worst={w:0,sel:""};for(const el of els){const r=el.getBoundingClientRect();if(r.width>worst.w){worst={w:r.width,sel:el.tagName.toLowerCase()+"."+[...el.classList].join(".")};}}return worst;});console.log(id);await b.close();})();'
{ w: 612.345703125, sel: 'div.value' }

Qué significa la salida: un elemento dentro de la rejilla (aquí div.value) es más ancho que el viewport.
Decisión: aplica truncamiento/envoltura a ese componente; no “arregles” encogiendo toda la rejilla.

Task 8: Confirma el comportamiento de envoltura para cadenas hostiles sin separadores

cr0x@server:~$ cat wrap-test.css
.value { overflow-wrap: anywhere; word-break: break-word; }
...output...

Qué significa la salida: permites saltos incluso en tokens largos.
Decisión: si los tokens deben seguir siendo copiable, prefiere truncamiento con UI de copiar al portapapeles; envolver un token de 64 caracteres en 5 líneas es correcto técnicamente y dañino emocionalmente.

Task 9: Valida la aritmética umbral de columnas con un script rápido

cr0x@server:~$ node -e 'const min=280, gap=16; for (const cols of [1,2,3,4,5]){console.log(cols, cols*min + (cols-1)*gap);} '
1 280
2 576
3 872
4 1168
5 1464

Qué significa la salida: los anchos mínimos de contenedor para cada recuento de columnas.
Decisión: compáralo con anchos reales del contenedor (Task 4). Si estás alrededor de 872px, espera fluctuación entre 2 y 3 columnas cuando cambie el chrome de la UI.

Task 10: Verifica si tus tarjetas se estiran verticalmente por alineación

cr0x@server:~$ node -e 'const { chromium } = require("playwright"); (async()=>{const b=await chromium.launch();const p=await b.newPage({viewport:{width:1200,height:800}});await p.goto("http://localhost:3000");const ai=await p.$eval(".grid", el=>getComputedStyle(el).alignItems);console.log(ai);await b.close();})();'
stretch

Qué significa la salida: los items pueden estirarse para igualar la altura de la fila.
Decisión: pon align-items: start en la rejilla si quieres alturas naturales de tarjeta.

Task 11: Detecta si las imágenes son las culpables del ancho intrínseco

cr0x@server:~$ node -e 'const { chromium } = require("playwright"); (async()=>{const b=await chromium.launch();const p=await b.newPage({viewport:{width:390,height:844}});await p.goto("http://localhost:3000");const imgs=await p.$$eval(".grid img", els=>els.map(i=>({w:i.getBoundingClientRect().width, css:getComputedStyle(i).maxWidth, src:i.getAttribute("src")})));console.log(imgs.slice(0,3));await b.close();})();'
[
  { w: 420, css: 'none', src: '/assets/chart.png' }
]

Qué significa la salida: una imagen es más ancha de lo debido y sin restricciones (max-width: none).
Decisión: aplica max-width: 100% y asegúrate de un dimensionado responsivo; de lo contrario una captura de gráfico puede arruinar toda la rejilla.

Task 12: Asegura que tu rejilla use gap, no márgenes que colapsen impredeciblemente

cr0x@server:~$ rg -n "\.card\{[^}]*margin" src/components | head
src/components/Card.css:.card{margin:12px}

Qué significa la salida: las tarjetas añaden márgenes que interfieren con el espaciado de la rejilla.
Decisión: elimina márgenes externos en items de la rejilla; usa gap en el contenedor grid para un espaciado consistente y predecible.

Task 13: Confirma que filas flex largas dentro de tarjetas pueden encoger

cr0x@server:~$ rg -n "display:\s*flex" -S src/components/Card* | head
src/components/CardMeta.css:.metaRow{display:flex;gap:8px}

Qué significa la salida: probablemente tienes hijos flex que pueden crear overflow por min-content.
Decisión: añade min-width: 0 en el elemento flex que debe encoger (a menudo el contenedor de texto), y truncamiento donde proceda.

Task 14: Prueba por “pista vacía” cambiando auto-fit/auto-fill

cr0x@server:~$ perl -0777 -pe 's/repeat\(auto-fit,/repeat(auto-fill,/g' -i src/styles/grid.css
...output...

Qué significa la salida: intercambiaste temporalmente el comportamiento.
Decisión: si la “última tarjeta solitaria” deja de estirarse, has demostrado que el colapso de pistas forma parte de tu elección UX. Vuelve atrás y decide intencionalmente.

Estas tareas parecen exageradas hasta que una regresión de layout rompe una demo de ventas. Entonces parecen seguro.

Listas de comprobación / plan paso a paso

Paso a paso: implementar una rejilla de tarjetas auto-fit segura para producción

  1. Define el ancho mínimo legible de la tarjeta.
    Usa contenido real. Si no lo tienes, usa las peores cadenas realistas (IDs, nombres, botones localizados).
  2. Empieza con la regla central:
    grid-template-columns: repeat(auto-fit, minmax(18rem, 1fr));
  3. Añade limitación defensiva del mínimo si la rejilla puede vivir en contenedores estrechos:
    minmax(min(18rem, 100%), 1fr).
  4. Usa gap en la rejilla, elimina márgenes de las tarjetas en ese contexto.
  5. Pon min-width: 0 en la tarjeta y en los hijos flex encogibles dentro de las tarjetas.
  6. Decide el ancho máximo de la tarjeta.
    Si las tarjetas se ven feas al estirarse, usa minmax(18rem, 24rem) y alinea con justify-content.
  7. Gestiona contenido hostil explícitamente.
    Trunca cadenas largas, limita líneas, restringe imágenes y gráficos.
  8. Prueba con zoom y paneles laterales.
    Trata la variabilidad del ancho del contenedor como una entrada de primera clase.
  9. Automatiza una barrida de viewports (como la Task 5) y falla builds si aparece overflow (Task 6).
  10. Documenta el contrato para los autores de tarjetas: no anchos fijos, no medios sin límites, y cómo manejar texto largo.

Lista de verificación: antes de añadir una media query

  • ¿Verificaste el ancho real del contenedor (no el ancho del viewport)?
  • ¿Hiciste la aritmética min+gap para los recuentos de columnas esperados?
  • ¿Añadiste min-width: 0 donde hace falta?
  • ¿Restringiste imágenes y gráficos al ancho de la tarjeta?
  • ¿Decidiste si las tarjetas deben estirarse (1fr) o mantenerse consistentes (max fijo)?
  • ¿Probaste con traducciones largas e identificadores extensos?
  • ¿Reprodujiste el bug con comprobaciones automatizadas de viewport/overflow?

Si no puedes marcar estas casillas, una media query es un parche sobre una fuga detrás de la pared.
Se verá bien hasta la próxima renovación.

Preguntas frecuentes

1) ¿Es repeat(auto-fit, minmax()) realmente “responsivo” sin media queries?

Sí, para cambios en el número de columnas impulsados por el espacio disponible. Responde al tamaño del contenedor, que es lo que realmente importa en apps complejas.
Las media queries responden al tamaño del viewport, que solo guarda relación laxa con el espacio que recibe tu rejilla.

2) ¿Debería usar auto-fit o auto-fill para tarjetas?

Por defecto usa auto-fit. Colapsa pistas vacías para que las tarjetas restantes se expandan naturalmente.
Usa auto-fill cuando quieras reservar columnas vacías intencionalmente (raro para tarjetas, común en calendarios).

3) ¿Por qué mi rejilla desborda aunque puse un ancho mínimo?

Porque las pistas del grid respetan el dimensionamiento intrínseco. Un hijo puede tener un min-content mayor que tu mínimo de pista.
Arréglalo con min-width: 0 en items de grid y hijos flex, además de envoltura/truncamiento para cadenas largas y restricciones para medios.

4) ¿Qué ancho mínimo debería elegir?

Elige el menor ancho en el que el contenido de la tarjeta siga siendo legible y operable. Para dashboards típicos, 280–360px es común.
Luego prueba con las peores cadenas y un contenedor estrecho, no solo con la vista de móvil.

5) A mis diseñadores les horrorizan las tarjetas estiradas. ¿Cuál es el patrón?

Usa un max width caps en minmax, como minmax(20rem, 24rem), y pon justify-content: center o start.
Alternativamente, limita el max-width del wrapper.

6) ¿Las container queries hacen esto obsoleto?

No. Las container queries ayudan cuando los cambios de layout dependen del tamaño del componente (p. ej., intercambiar internos de la tarjeta).
Para “cuántas columnas caben”, auto-fit/minmax sigue siendo el primitivo más simple y robusto.

7) ¿Cómo evito el layout shift cuando se cargan datos?

Haz que los esqueletos coincidan con tamaños de contenido típicos, no idealizados. Restringe contenido variable (line clamps, ratio fijo para gráficos).
No congeles tan duro los tamaños de tarjeta que el contenido real desborde o quede recortado.

8) ¿Por qué cambia el número de columnas cuando aparece una barra de desplazamiento?

Las barras de desplazamiento consumen espacio inline, empujando el contenedor por debajo de un umbral donde cabe una columna menos.
Date margen: reduce ligeramente el ancho mínimo o el gap, o estabiliza el espacio de la scrollbar con CSS donde tenga sentido.

9) ¿Puedo hacer un layout tipo masonry con esto?

No realmente. Las filas del grid se alinean; masonry necesita colocación vertical independiente.
Puedes simular un aspecto masonry con packing denso en algunos casos, pero para UI de producción donde importa el orden y la escaneabilidad, evita masonry en tarjetas con acciones.

10) ¿Cuál es la solución de “una línea” más importante para rarezas en la rejilla?

min-width: 0 en items de grid (y en hijos flex encogibles). Evita que el contenido obligue a las pistas a ser más anchas de lo previsto.
Es el equivalente CSS de “¿comprobaste el DNS?”—molesto, pero a menudo correcto.

Conclusión: siguientes pasos que puedes enviar

La era de los breakpoints enseñó a la gente a tratar el layout como un conjunto de regímenes codificados. Eso funciona hasta que tu app tiene chrome real, contenido real, niveles de zoom reales y usuarios reales.
Las rejillas de tarjetas son el lugar perfecto para cambiar a un layout basado en restricciones.

Haz esto a continuación, en orden:

  1. Implementa repeat(auto-fit, minmax(min(X, 100%), 1fr)) con un X sensato basado en contenido legible.
  2. Pon min-width: 0 en tarjetas y en hijos flex internos que encogen.
  3. Decide si quieres tarjetas estiradas (1fr) o anchos consistentes (max limitado).
  4. Añade comprobaciones automatizadas de overflow y recuento de columnas en distintos viewports—trata las regresiones de layout como regresiones operativas.

Si haces esas cuatro cosas, escribirás menos media queries, publicarás menos layouts de “funciona en mi máquina” y pasarás menos tiempo depurando fantasmas causados por anchos de contenedor que no mediste.
Eso es el verdadero lujo.

← Anterior
Correo: Confusión entre S/MIME y TLS — Qué mejora realmente la seguridad
Siguiente →
WordPress bloqueado por WAF: afina reglas sin crear brechas de seguridad

Deja un comentario