Fragment: agrupar sin nodo extra
El componente Fragment y la sintaxis abreviada para agrupar varios elementos sin emitir un contenedor, su papel como anfitrion de directivas como set:html, y por que existe solo en tiempo de compilacion sin dejar rastro en el DOM.
A veces necesitas devolver varios elementos donde la sintaxis exige uno solo, o agrupar hermanos sin ensuciar el DOM con un <div> de más. Para eso existe Fragment: un nodo que no es un nodo, un contenedor que no deja rastro en el HTML. Es una pieza pequeña, pero resuelve una tensión real entre cómo escribes el código y qué árbol quieres emitir.
- Agrupar varios elementos hermanos con
<Fragment>sin añadir un contenedor al DOM. - Usar la sintaxis abreviada de fragmento y conocer su única limitación.
- Emplear
Fragmentcomo anfitrión de directivas comoset:htmlsin envoltorio. - Entender que
Fragmentvive solo en compilación y no produce ningún elemento.
El problema: una sola raíz, muchos hermanos
Cuando una expresión —una rama de ternario, una iteración de .map— debe producir varios elementos hermanos, no puedes soltarlos sueltos: la sintaxis pide una única raíz. La salida obvia es envolverlos en un <div>, pero ese <div> es real y tiene consecuencias: aparece en el DOM, puede romper el grid o el flex del padre, alterar la semántica del documento o invalidar la estructura de una <table>, donde solo caben ciertos hijos. Fragment agrupa sin pagar ninguno de esos costes.
---
const items = ['uno', 'dos'];
---
<dl>
{items.map((it) => (
<Fragment>
<dt>{it}</dt>
<dd>Descripción de {it}</dd>
</Fragment>
))}
</dl>
Aquí cada iteración produce dos elementos, <dt> y <dd>, que deben ser hijos directos de <dl>. Un <div> envolvente rompería esa relación padre-hijo que el HTML de las listas de definición exige. <Fragment> los agrupa para satisfacer la sintaxis y luego desaparece, dejando los <dt> y <dd> colgando directamente del <dl>, como el estándar manda.
El caso de las tablas es aún más tajante, porque ahí un envoltorio no solo estorba: produce HTML inválido. Un <tr> solo admite <td> o <th> como hijos, de modo que envolver varias celdas en un <div> corrompe la tabla y el navegador la reinterpreta. <Fragment> es la única forma limpia de emitir varias celdas por iteración sin colar un contenedor prohibido:
<tr>
{columnas.map((c) => (
<Fragment>
<td>{c.clave}</td>
<td>{c.valor}</td>
</Fragment>
))}
</tr>
La sintaxis abreviada
Existe una forma corta del fragmento, azúcar del mismo <Fragment>, que se escribe con etiquetas de apertura y cierre vacías. Es cómoda para agrupaciones triviales, pero tiene una limitación tajante: no admite atributos ni directivas. En el momento en que necesites poner algo en el fragmento —un set:html, un slot— tienes que volver a la forma nombrada.
<>
<span>Rápido</span>
<span>y limpio</span>
</>
La forma abreviada no puede llevar atributos, así que en cuanto un fragmento necesite set:html, set:text o slot="nombre", cambia a <Fragment ...>. La regla es simple: la forma corta es para agrupar y nada más; la forma nombrada es la que hace trabajo. Confundirlas produce el error críptico de intentar colgar una directiva de algo que no la acepta.
Fragment como anfitrión de directivas
Aquí está el uso más idiomático y menos obvio. Algunas directivas necesitan un elemento del que colgarse, pero tú no quieres el elemento, solo su efecto. set:html, que inyecta una cadena de HTML, es el caso típico: si la cuelgas de un <div>, te llevas el <div> de propina; si la cuelgas de un <Fragment>, inyectas el HTML sin envoltorio alguno.
---
const renderizado = '<strong>Contenido</strong> desde un CMS';
---
<Fragment set:html={renderizado} />
El resultado en el DOM es directamente <strong>Contenido</strong> desde un CMS, sin ningún elemento contenedor alrededor. Fragment actúa como un anfitrión sin cuerpo: recibe la directiva, ejecuta su efecto y no deja marca. El mismo patrón sirve para reenviar varios elementos a un slot con nombre, cuando no quieres un envoltorio rodeando lo que pasas al componente hijo.
---
// el layout hijo expone un hueco: <slot name="cabecera" />
---
<Fragment slot="cabecera">
<h1>Título</h1>
<p>Subtítulo</p>
</Fragment>
Sin el Fragment, tendrías que envolver el título y el subtítulo en un <div slot="cabecera">, metiendo en la cabecera del layout un contenedor que quizá no querías. Con Fragment, ambos elementos entran al slot como hermanos, y el layout los coloca sin envoltura intermedia. Es el mismo principio de siempre: agrupar para la sintaxis, desaparecer en la salida.
Solo existe en compilación
La clave para entender Fragment es que no es un elemento diferido ni invisible: sencillamente no existe en la salida. El compilador de Astro lo lee como una instrucción de agrupación —“trata a estos hijos como un bloque”— y luego lo descarta, emitiendo solo sus hijos. No hay etiqueta oculta, ni comentario, ni nodo vacío en el HTML: cero rastro. Es lo que lo separa de un <span style="display:contents">, que sí es un nodo real con sus propios efectos y su presencia en el árbol.
El contraste se ve mejor con la salida lado a lado. Un contenedor deja su etiqueta en el DOM; el fragmento no deja ninguna, y sus hijos suben un nivel para colgar directamente del padre real:
<!-- envuelto en un div: el div sobrevive en el DOM -->
<div><dt>uno</dt><dd>...</dd></div>
<!-- agrupado con Fragment: solo quedan los hijos -->
<dt>uno</dt><dd>...</dd>
Conceptualmente se parece al fragmento de React —ninguno de los dos pinta envoltorio—, pero el de Astro se usa a menudo para un papel que el de React no tiene: ser el anfitrión de una directiva de plantilla. React inyecta HTML crudo con dangerouslySetInnerHTML sobre un elemento real, mientras que Astro te deja hacerlo sobre un fragmento que se evapora. Esa diferencia es pequeña en apariencia y reveladora en el fondo: en Astro, hasta el mecanismo para inyectar contenido puede no dejar contenedor.
Conviene no confundir un Fragment con un elemento oculto. Un <span style="display:contents"> desaparece visualmente, pero sigue en el DOM: ocupa un nodo, cuenta para los selectores, aparece en el árbol de accesibilidad. Fragment no está oculto: no está, punto. La diferencia importa cuando cuentas hijos con nth-child, recorres el DOM por script o te apoyas en la estructura exacta del árbol. Uno es un nodo que finge no verse; el otro nunca llegó a existir.
flowchart LR A[Fragment agrupa dt y dd] --> B[compilador de Astro] B --> C[descarta el nodo Fragment] C --> D[DOM solo con dt y dd] style A fill:#89b4fa,color:#11111b style D fill:#a6e3a1,color:#11111b
Fragment parece un detalle sintáctico, pero encierra una de las ideas más profundas de cualquier compilador: que la estructura con la que escribes no tiene por qué ser la estructura que emites. La sintaxis de plantillas te impone una gramática —una sola raíz por expresión, hijos que cuelgan de un padre— que existe para que el código sea legible y analizable, no porque el HTML resultante la necesite. Fragment es la válvula que libera esa tensión: te deja satisfacer la gramática agrupando elementos bajo un nodo, y luego borra ese nodo, entregando al navegador exactamente el árbol que querías, ni un <div> de más. Es un nodo cuyo único propósito es dejar de existir. Esa naturaleza —presente en tu fuente, ausente en tu salida— es la firma de la compilación frente a la interpretación: en un lenguaje interpretado, lo que escribes suele ser lo que corre; en uno compilado, tu código es materia prima que una máquina transforma, y algunas construcciones están ahí solo para guiar esa transformación antes de evaporarse. Fragment te enseña a pensar en esos dos planos a la vez: el de la conveniencia de autoría, donde agrupar te ordena el código y te permite colgar una directiva de algo, y el de la salida emitida, donde cada elemento pesa —en bytes, en semántica, en cómo se comporta el layout— y por tanto conviene que solo exista lo que de verdad quieres. Cuando un <div> envolvente te rompe un grid o ensucia la semántica de una tabla, no estás peleando con Astro: estás descubriendo que confundiste el andamio con el edificio. Fragment es Astro recordándote que el andamio puede desmontarse en cuanto ha cumplido su función.
- Mapea una lista donde cada iteración produzca un
<dt>y un<dd>, agrupados con<Fragment>; verifica en el HTML que no hay contenedor entre ellos y que cuelgan directos del<dl>. - Reescribe una agrupación trivial con la sintaxis abreviada de fragmento y confirma que se comporta igual que la nombrada.
- Intenta poner
set:htmlen la forma abreviada y observa por qué necesitas la forma nombrada<Fragment set:html={...}>para colgar la directiva. - Compara el DOM resultante de envolver dos elementos en un
<div>y en un<Fragment>; anota qué nodo aparece en un caso y desaparece en el otro.