wandres.dev
PROPS Y SLOTS · componer componentes

Fallback y slots avanzados

Cómo dar contenido de reserva a un hueco con un slot de apertura y cierre, comprobar en el frontmatter si un slot recibió contenido con Astro.slots.has, renderizar un slot a una cadena de HTML con Astro.slots.render para inspeccionarlo o transformarlo, y proyectar componentes completos como contenido hijo para anidar composiciones.

⏱ 16 min

Un slot no tiene por qué ser un hueco pasivo que se llena o se queda vacío. Puede traer su propio contenido de reserva, puede consultarse desde el frontmatter para adaptar la estructura a lo que llegó, e incluso puede convertirse en una cadena de HTML que el componente inspecciona y transforma. Y lo que se proyecta no se limita a marcado suelto: cabe un componente entero. Esta lección recorre el borde donde la composición deja de ser declarativa y se vuelve programable.

🎯 Al terminar esta lección sabrás
  • Definir contenido de reserva dentro de un <slot> con etiqueta de apertura y cierre.
  • Comprobar si un hueco recibió contenido con Astro.slots.has() en el frontmatter.
  • Renderizar un slot a una cadena de HTML con Astro.slots.render() para medirlo o transformarlo.
  • Proyectar componentes completos como contenido hijo y anidar composiciones sin límite.

Fallback: el contenido de reserva

Un <slot /> autocerrado deja el hueco vacío si nadie lo rellena. Pero si le das etiqueta de apertura y cierre, lo que escribas dentro se convierte en su contenido de reserva: aparece solo cuando el padre no proyecta nada para ese hueco.

---
// src/components/Boton.astro
---
<button type="button">
  <slot>Aceptar</slot>
</button>

Si el padre escribe <Boton>Guardar</Boton>, gana Guardar; si escribe <Boton /> sin hijos, el botón muestra Aceptar. La semántica es de respaldo, no de suma: el contenido de reserva y el del padre nunca conviven, o uno o el otro. Funciona igual en un hueco con nombre, que puede llevar su propio respaldo dentro.

<header>
  <slot name="titulo"><h1>Documento sin título</h1></slot>
</header>

El fallback es a los slots lo que el valor por defecto es a las props: una red de seguridad que hace opcional lo que de otro modo sería obligatorio. Con él, un componente rinde algo sensato incluso cuando lo usan a medias, y el diseño gana robustez sin que el padre tenga que acordarse de rellenar cada zona.

Astro.slots.has: interrogar el hueco

El fallback rellena un hueco vacío, pero a veces quieres lo contrario: no renderizar la estructura en absoluto cuando no hay contenido. Para eso, el frontmatter puede preguntar si un slot recibió algo con Astro.slots.has.

---
// src/components/Panel.astro
const tienePie = Astro.slots.has('pie');
---
<section class="panel">
  <slot />
  {tienePie && (
    <footer class="panel-pie">
      <slot name="pie" />
    </footer>
  )}
</section>

Astro.slots.has('pie') devuelve un booleano: ¿proyectó el padre contenido para pie? Con esa respuesta decides si el <footer> llega a existir. Ahí está la diferencia con el fallback: el respaldo llena el agujero con algo; has te deja eliminar el agujero entero, sin un <footer> vacío cuando no hay pie. El fallback responde a una pregunta —qué mostrar cuando el hueco llega vacío—; has responde a otra —si esa estructura debe existir siquiera—. Para el slot por defecto se consulta con el nombre default.

💡
Fallback y has resuelven problemas opuestos

No compiten: se reparten el trabajo. Usa el fallback cuando siempre quieres algo en esa posición y solo cambia el qué —un título por defecto, una etiqueta genérica en un botón—. Usa has cuando la propia envoltura sobra si no hay contenido —un <footer> con borde, una barra lateral con márgenes— y renderizarla vacía ensuciaría la maqueta. Uno rellena el hueco; el otro decide si el hueco merece existir.

Astro.slots.render: el slot como valor

Astro.slots.render da el paso más radical: convierte el contenido proyectado en un valor. Devuelve una promesa que, al resolverse, entrega el HTML ya renderizado del slot como una cadena de texto.

---
// src/components/Extracto.astro
const html = await Astro.slots.render('default');
const texto = html.replace(/<[^>]+>/g, '');
const palabras = texto.trim().split(/\s+/).filter(Boolean).length;
---
<article data-palabras={palabras}>
  <Fragment set:html={html} />
</article>

Con la cadena en la mano, el contenido deja de ser algo que solo colocas y pasa a ser algo que examinas: puedes medirlo, contar sus palabras, retirar sus etiquetas, transformarlo y devolverlo al DOM con set:html. Es el salto de posicionar contenido a inspeccionarlo. La contrapartida es que asumes la responsabilidad de reinyectarlo tú mismo: si no colocas el resultado con set:html, no aparece en ningún sitio.

Conviene medir el uso. Astro.slots.render obliga a esperar con await y a manejar una cadena, así que no sale gratis en complejidad; recúrrelo cuando de verdad necesites el contenido como dato, no como atajo para colocarlo. Para simplemente mostrar lo proyectado, <slot /> sigue siendo la herramienta correcta y más barata.

⚠️
set:html no escapa nada

set:html inserta la cadena tal cual, sin escaparla. Con la salida de Astro.slots.render el riesgo es bajo, porque ese HTML lo generó tu propio árbol de componentes a partir de contenido que tú controlas. Pero si mezclas en esa cadena texto de origen externo —parámetros de la URL, datos de un formulario, respuestas de una API—, set:html lo inyectaría sin filtrar y abrirías la puerta a un XSS. La disciplina: reinyecta solo lo que tú produjiste, nunca lo que llegó de fuera sin sanear.

📝
render acepta parámetros para los slots función

Astro.slots.render admite un segundo argumento, un array de parámetros. Si el contenido del slot se escribió como una función, esos parámetros se le entregan al invocarla, lo que permite que el componente alimente con datos al contenido que recibe. Es una técnica avanzada y poco frecuente, pero rompe la unidireccionalidad habitual del slot: el hueco deja de ser un receptor mudo y puede pasar valores hacia quien lo rellena. Reserva este poder para casos reales; casi siempre, un slot corriente basta.

Componentes como contenido hijo

Nada obliga a que los hijos sean HTML nativo. Un componente entero cabe como contenido proyectado, y ahí la composición despliega toda su fuerza.

---
// src/pages/index.astro
import Modal from '../components/Modal.astro';
import Boton from '../components/Boton.astro';
---
<Modal>
  <h2 slot="titulo">Confirmar borrado</h2>
  <p>Esta acción no se puede deshacer.</p>
  <Boton slot="acciones">Borrar</Boton>
</Modal>

Boton se renderiza en el ámbito de la página y aterriza en el hueco acciones de Modal, que ni conoce ni necesita conocer qué componente ha recibido. Este es el mecanismo detrás de un layout que envuelve una página que envuelve widgets: son slots hasta el fondo. Cada capa aporta estructura y cede contenido, y la composición se anida tan hondo como el diseño pida, sin que ninguna capa dependa de las que aloja.

Anidar composiciones es solo apilar esta misma operación. Un Layout con su <slot /> envuelve una página, y esa página envuelve a su vez componentes en los slots de otras piezas, formando una pila donde cada nivel presta estructura al de dentro.

---
// src/pages/acerca.astro
import Layout from '../layouts/Base.astro';
import Tarjeta from '../components/Tarjeta.astro';
---
<Layout>
  <Tarjeta>
    <h2 slot="cabecera">Sobre nosotros</h2>
    <p>Un equipo pequeño construyendo herramientas sobrias.</p>
  </Tarjeta>
</Layout>
flowchart LR
CONT[Contenido del hueco] --> HAS[Astro.slots.has decide si existe la zona]
CONT --> REN[Astro.slots.render entrega el HTML como cadena]
REN --> VAL[Medir transformar o reinyectar con set html]
style CONT fill:#89b4fa,color:#11111b
style VAL fill:#a6e3a1,color:#11111b
Cuando el contenido se vuelve dato

Hay una frontera conceptual que estas herramientas cruzan, y conviene verla con claridad. En su forma básica, un slot es declarativo: el padre proyecta, el hijo posiciona, y ninguno mira dentro del contenido del otro. Esa opacidad mutua es justo lo que hace robusta la composición, porque desacopla a las partes. Astro.slots.render perfora esa opacidad: entrega al hijo el contenido del padre como una cadena que puede leer, medir, recortar y reescribir. En ese instante el contenido deja de ser marcado inerte y se vuelve dato, y el componente pasa de anfitrión a autor. Es un poder genuino —habilita extractos automáticos, tablas de contenidos, recuentos, transformaciones imposibles de otro modo— y, como todo poder, cobra un precio. Un componente que inspecciona lo que se le proyecta se acopla a la forma de ese contenido: empieza a suponer que dentro hay párrafos, o encabezados, o cierta estructura, y esa suposición es una grieta por donde entran los fallos cuando alguien lo usa de un modo que no previste. Por eso la regla de oro es de proporción: usa la forma declarativa siempre que puedas y reserva render y has para cuando de verdad necesites que la estructura reaccione a su contenido. has es el uso disciplinado —preguntar si algo existe para decidir si envolverlo— y apenas mira dentro; render es el uso ambicioso —tratar el contenido como materia prima— y mira del todo. Entre ambos se dibuja el espectro de la composición: de un lado, piezas que se ignoran y por eso encajan con cualquiera; del otro, piezas que se leen y por eso logran lo que la mera yuxtaposición no alcanza, a cambio de atarse a lo que leen. Dominar los slots no es solo saber proyectar contenido, sino tener el juicio de decidir cuánto debe una pieza saber de aquello que aloja.

⚔️ Del respaldo al slot programable
  1. Da a un <slot> de un componente un contenido de reserva y úsalo dos veces: una entregando hijos y otra sin ellos; confirma que solo la segunda muestra el respaldo.
  2. Envuelve una zona opcional en una condición con Astro.slots.has y verifica que su <footer> no aparece en el HTML cuando no proyectas nada para ese hueco.
  3. En un componente, captura el slot por defecto con await Astro.slots.render('default'), cuenta sus palabras y expónlas en un atributo data-palabras; reinyecta el contenido con set:html.
  4. Proyecta un componente entero como hijo de otro —un <Boton> dentro de un <Modal>— y comprueba que se renderiza con las variables del padre, no del hijo que lo aloja.