El problema real que resuelve subgrid
Tres tarjetas hermanas con títulos de longitudes distintas y botones que no se alinean: por qué ningún grid normal puede resolverlo, y por qué todos los apaños previos fallaban.
Tres tarjetas en fila. La primera tiene un título de una línea, la segunda de dos, la tercera de tres. Los botones de la parte inferior quedan a tres alturas distintas y el conjunto se ve desordenado. Este problema tiene veinte años, ha generado media docena de apaños que fallan de formas distintas, y es la razón exacta por la que subgrid existe. Entender por qué grid por sí solo no puede resolverlo es entender la limitación estructural que subgrid levanta.
- Describir el problema de alineación entre tarjetas hermanas con precisión.
- Explicar por qué un grid dentro de otro no lo resuelve.
- Enumerar los apaños históricos y el modo concreto en que falla cada uno.
- Reconocer el patrón en otros contextos además de las tarjetas.
El caso
<ul class="tarjetas">
<li class="tarjeta">
<h3>Plan básico</h3>
<p>Para empezar.</p>
<a class="cta" href="/basico">Elegir</a>
</li>
<li class="tarjeta">
<h3>Plan profesional con nombre largo</h3>
<p>Para equipos que necesitan colaboración, control de accesos y soporte.</p>
<a class="cta" href="/pro">Elegir</a>
</li>
<li class="tarjeta">
<h3>Empresa</h3>
<p>Todo lo anterior más facturación anual.</p>
<a class="cta" href="/empresa">Elegir</a>
</li>
</ul>
.tarjetas {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1.5rem;
list-style: none;
padding: 0;
}
.tarjeta {
border: 1px solid #45475a;
border-radius: 0.75rem;
padding: 1.25rem;
}
Las tarjetas tienen la misma altura, porque align-items: stretch es el valor por defecto y las estira a la altura de la fila. Pero dentro de cada una, el título ocupa lo que ocupa, la descripción empieza justo debajo, y el enlace queda pegado al final del texto. Con títulos de una, dos y tres líneas, los tres enlaces quedan a alturas distintas y las descripciones también empiezan a alturas distintas.
El resultado se ve mal, y el motivo por el que se ve mal es el mismo que hace que una tabla se lea bien: la alineación horizontal comunica que dos cosas son comparables. Cuando los precios de tres planes están a alturas distintas, el ojo tiene que buscarlos en lugar de compararlos.
Por qué grid no lo resuelve
La respuesta obvia es convertir cada tarjeta en un grid con tres filas. No funciona, y el motivo es estructural.
/* Cada tarjeta es un grid... pero de tres grids independientes */
.tarjeta {
display: grid;
grid-template-rows: auto auto auto;
}
Cada tarjeta establece su propio contexto de formato de grid. La fila 1 de la primera tarjeta y la fila 1 de la segunda no son la misma pista: son dos pistas de dos rejillas distintas que no comparten nada. Cada una se dimensiona con su propio contenido, con el algoritmo de cinco fases ejecutado por separado.
Es exactamente el mismo muro que ya viste en flex-wrap: las líneas de un contenedor flex no se hablan entre sí. Aquí las rejillas hermanas no se hablan entre sí. La información necesaria para alinear —cuánto mide el título más alto de las tres— no está disponible dentro de ninguna de las tres tarjetas.
Lo único que sí es común es la rejilla del contenedor .tarjetas, porque sus pistas sí las comparten las tres. Pero los elementos que quieres alinear no son hijos de ese contenedor: son nietos. Y hasta subgrid no había forma de que un nieto participara en las pistas del abuelo.
Aparece en más sitios de los que parece: filas de un formulario donde la etiqueta y el campo deben alinearse aunque las etiquetas tengan longitudes distintas; una lista de características con icono, nombre y valor; una tabla de comparación construida con divs; una fila de artículos con imagen, título y metadatos. En todos, el patrón es el mismo: elementos hermanos cuyo contenido interno debe alinearse entre ellos.
Los apaños y cómo fallan cada uno
Altura fija en el título. block-size: 3rem en el h3 alinea todo perfectamente hasta que un título necesita cuatro líneas o el usuario aumenta el tamaño de letra. Entonces el texto se desborda o se recorta. Falla con contenido que no controlas, que es casi todo.
Recortar con line-clamp. -webkit-line-clamp: 2 fuerza dos líneas y pone puntos suspensivos. Alinea, pero pierde información: un título que no cabe se corta, y ese título probablemente era el que mejor describía el producto. Además el recorte no es accesible: el lector de pantalla anuncia el texto completo, con lo que lo que se oye y lo que se ve difieren.
Empujar con flex. Cada tarjeta en display: flex; flex-direction: column, y el enlace con margin-block-start: auto. Esto sí funciona para el último elemento: los tres enlaces quedan alineados abajo. Pero no alinea nada intermedio: las descripciones siguen empezando a alturas distintas. Resuelve un tercio del problema.
Medir con JavaScript. Un ResizeObserver que mide el título más alto y aplica esa altura a todos. Funciona, y es lo que hacían las librerías de igualación de alturas durante años. Los problemas son tres: ocurre después del primer pintado, con lo que hay un salto visible; se recalcula en cada cambio de tamaño, con coste; y falla en cuanto el contenido cambia sin que el observador se entere, por ejemplo al cargar una fuente web.
Convertir las tarjetas en una tabla. Alinea perfectamente, porque una tabla sí comparte las filas entre todas las columnas. Y destroza la semántica, la accesibilidad y cualquier posibilidad de que las tarjetas envuelvan en móvil.
Cinco apaños, cinco modos de fallo distintos, y ninguno resuelve el caso general.
El diagnóstico
El patrón que todos comparten se puede enunciar de una forma que resulta útil: necesitas que un contenido alineado entre hermanos participe de un dimensionado común, pero la estructura del DOM interpone un nivel entre el contenido y el contenedor que define ese dimensionado.
Formulado así, la solución que hace falta es evidente: una manera de que ese nivel intermedio sea transparente para el dimensionado. Que la tarjeta no cree su propia rejilla sino que preste las pistas del abuelo a sus hijos.
Eso es exactamente subgrid, y es la razón de que su nombre sea ese. No es una funcionalidad nueva de layout: es un mecanismo de herencia de pistas que atraviesa un nivel del árbol.
Hubo una alternativa parcial antes: display: contents, que hace desaparecer la caja del elemento y promueve a sus hijos a elementos del abuelo. Alinea, porque los nietos pasan a ser hijos de verdad. Y tiene un problema que lo descarta aquí: al desaparecer la caja de la tarjeta, desaparecen su borde, su fondo, su relleno y su radio. Una tarjeta sin caja no es una tarjeta.
Merece la pena detenerse en lo que este caso revela sobre la relación entre HTML y CSS. Durante toda la historia de la web, la regla implícita ha sido que el layout solo puede operar entre hermanos: un contenedor coloca a sus hijos y ahí acaba su autoridad. Cualquier relación visual entre elementos que no son hermanos había que construirla artificialmente, aplanando el DOM hasta que los elementos que querías relacionar acabaran siendo hijos del mismo padre. De ahí vienen la mayor parte de los marcados enrevesados que has visto en tu vida: divisiones que existen solo porque el layout las exigía, jerarquías inventadas para que un contenedor pudiera alcanzar lo que necesitaba colocar. El coste de eso no es estético: es que la estructura semántica del documento —la que usan los lectores de pantalla, los buscadores y cualquiera que lea el HTML— acaba deformada por necesidades de presentación, que es justo lo que la separación entre HTML y CSS prometía evitar. Subgrid es la primera herramienta que rompe esa restricción de forma limpia, permitiendo que la autoridad de un contenedor atraviese un nivel sin que ese nivel desaparezca. Y por eso importa más de lo que su tamaño sugiere: no añade un layout nuevo, devuelve libertad al marcado. Cuando evalúes una funcionalidad de CSS, esa es una pregunta que rinde mucho: ¿me permite escribir un HTML más honesto? Las que responden que sí —gap, :has(), container queries, subgrid— acaban cambiando cómo escribes todo lo demás.