El slot por defecto y la composición
Cómo un componente recibe contenido hijo mediante el marcador de slot, en qué se diferencia la composición del paso de props, dónde se sitúa el hueco dentro de la plantilla del hijo y por qué el contenido proyectado se evalúa en el ámbito léxico del padre que lo redacta y no del hijo que lo aloja.
Las props configuran un componente con datos; los slots lo rellenan con contenido. Es un segundo canal, complementario del primero: en lugar de pasar un texto o un número por un atributo, el padre entrega un fragmento entero de plantilla —marcado, otros componentes, lo que haga falta— y el hijo decide en qué punto de su estructura colocarlo. Ese mecanismo, la proyección de contenido, es el fundamento de la composición: construir piezas que envuelven a otras sin saber de antemano qué envuelven.
- Entregar contenido hijo a un componente escribiéndolo entre sus etiquetas de apertura y cierre.
- Recibir y posicionar ese contenido con el marcador
<slot />en la plantilla del hijo. - Distinguir la composición mediante slots del paso de datos mediante props.
- Comprender que el contenido proyectado se evalúa en el ámbito léxico del padre, no del hijo.
Contenido hijo: lo que va entre las etiquetas
Hasta ahora instanciabas componentes con la etiqueta autocerrada, como <Saludo />. Pero un componente admite también etiqueta de apertura y de cierre, y todo lo que escribas entre ambas es su contenido hijo: un fragmento de plantilla que el padre redacta y confía al hijo para que lo aloje.
---
// src/pages/index.astro
import Aviso from '../components/Aviso.astro';
---
<Aviso>
<p>Tu sesión caduca en cinco minutos.</p>
</Aviso>
Ese <p> no es una prop. No viaja por un atributo ni aparece en Astro.props; es marcado que el padre deposita dentro del hijo. Puedes proyectar varios nodos a la vez —varios elementos, texto suelto, incluso otros componentes— y todos entran juntos, en el orden en que los escribiste. Pero aquí surge la primera regla implacable: si Aviso no hace nada para recogerlo, el contenido se descarta en silencio. Escribir hijos no basta; el hijo tiene que reclamarlos.
Conviene una distinción fina antes de seguir. <Aviso /> autocerrado y <Aviso></Aviso> vacío son equivalentes: ninguno proyecta nada, y el hijo se comporta como si nadie le hubiera pasado contenido. La etiqueta de cierre no obliga a rellenar el hueco, solo abre la posibilidad; se usa cuando hay algo que alojar dentro y se omite —autocerrando— cuando el componente se configura únicamente por props.
El contenido hijo y las props viajan por vías separadas. Una prop es un valor con nombre que llega por Astro.props; los hijos son marcado anónimo que llega por el hueco del slot. Un mismo componente puede recibir ambos a la vez —props que lo configuran y contenido que lo rellena— sin que uno interfiera con el otro. Son dos entradas distintas de la misma pieza, y conviene no mezclarlas mentalmente.
El marcador <slot />
El hijo reclama su contenido con un elemento especial: <slot />. Funciona como un marcador de posición que Astro sustituye por lo que el padre haya colocado entre las etiquetas.
---
// src/components/Aviso.astro
---
<aside class="aviso" role="status">
<slot />
</aside>
El componente aporta la envoltura —el <aside>, su clase, su rol de accesibilidad— y reserva con <slot /> el hueco por donde entrará lo de fuera. El resultado combina ambas capas: la estructura del hijo por fuera, el contenido del padre por dentro. Coloca el marcador en el punto exacto que tu estructura destina al contenido ajeno; su posición en la plantilla es toda su responsabilidad, porque el <slot /> no decide qué recibe, solo dónde aparece.
La envoltura que rodea al hueco puede ser tan rica como quieras: varios elementos anidados, clases, atributos de accesibilidad, incluso otros componentes. El <slot /> es solo el punto de inyección dentro de esa envoltura, no un límite para ella. Un componente puede tener una cabecera fija, un pie fijo y, en medio, el hueco por donde entra lo que cambia de un uso a otro.
La proyección de contenido no es una idea propia de Astro: es el mismo mecanismo del elemento <slot> de los Web Components estándar y el primo hermano de la prop children de React. En los tres casos un componente reserva un hueco y quien lo usa lo rellena. La diferencia de Astro es que la costura ocurre en tiempo de compilación, en el servidor, y no cuesta ni un byte de JavaScript en el cliente. El <slot /> es una junta del build, no un mecanismo del navegador.
Solo puede haber un slot por defecto —el que no lleva name— porque el contenido sin marcar necesita un único destino inequívoco. Puedes acompañarlo de cuantos huecos con nombre quieras, algo que verás en la próxima lección, pero el hueco anónimo es siempre uno. Si tu diseño parece pedir dos zonas sin nombre, es la señal de que al menos una debería llevarlo.
Composición frente a props
Props y slots resuelven necesidades distintas, y confundirlas produce diseños torpes. Una prop transporta un dato —un valor tipado y atómico que el hijo interpreta— bajo un contrato estrecho. Un slot transporta contenido —marcado arbitrario, quizá con otros componentes dentro— que el hijo no interpreta, solo ubica.
Props: datos
Valores tipados por interface Props. El hijo los lee y decide qué hacer con ellos: compararlos, iterarlos, componer clases. Contrato estrecho y verificado.
Slots: contenido
Fragmentos de plantilla que el hijo no inspecciona, solo coloca. Contrato abierto de forma: cabe cualquier marcado que el padre quiera proyectar.
La regla práctica es directa. Si el hijo necesita entender el valor —compararlo con una lista, meterlo en un atributo, recorrerlo—, es una prop. Si el hijo solo necesita envolver algo cuya forma no le incumbe —una tarjeta que rodea cualquier cuerpo, un botón que muestra cualquier etiqueta—, es un slot. El estado que gobierna una variante entra por props; el interior de un modal entra por slot.
Lo más común es que un mismo componente use ambos canales a la vez. Un botón recibe por props su variante —un dato que interpreta para elegir una clase— y por slot su etiqueta —contenido que solo muestra sin mirarlo.
---
// src/components/Boton.astro
interface Props {
variante?: 'solida' | 'fantasma';
}
const { variante = 'solida' } = Astro.props;
---
<button class={`btn-${variante}`}>
<slot />
</button>
El padre escribe <Boton variante="fantasma">Cancelar</Boton>: variante viaja por el atributo y termina traducida en una clase; Cancelar viaja por el hueco y termina como texto visible. Cada canal hace lo suyo y el componente los combina sin confundirlos. Reconocer qué parte de una pieza es dato y qué parte es contenido es, en el fondo, todo el arte de diseñar su interfaz.
Este molde —una envoltura de estructura fija con un hueco central— reaparece en casi toda biblioteca de componentes: la tarjeta, el modal, el aviso, el acordeón, el propio botón. Todos comparten la misma anatomía, props para lo que interpretan y un slot para lo que albergan, y por eso, en cuanto entiendes el patrón en uno, lo reconoces en todos.
El ámbito del contenido proyectado
Queda una pregunta sutil pero decisiva: cuando el contenido hijo usa una variable, ¿de quién es esa variable, del padre o del hijo? La respuesta define la mitad del modelo mental de los slots.
---
// src/pages/perfil.astro
import Panel from '../components/Panel.astro';
const usuario = 'Ada';
---
<Panel>
<h2>Hola, {usuario}</h2>
</Panel>
El {usuario} de ese <h2> se resuelve con la constante usuario de la página, porque el contenido hijo se redacta en la página y se evalúa con su ámbito léxico. Aunque Panel definiera dentro su propio usuario, no influiría: el hijo solo elige la posición del fragmento, mientras que los nombres que ese fragmento usa se buscan siempre arriba, donde fue escrito. El hijo hereda el marcado ya resuelto, no el código que lo produjo.
flowchart TD PAD[Pagina padre redacta el contenido hijo] --> EVAL[Se evalua en el ambito lexico del padre] EVAL --> SLOT[Marcador slot en el componente hijo] SLOT --> OUT[HTML final con el contenido incrustado] style PAD fill:#89b4fa,color:#11111b style OUT fill:#a6e3a1,color:#11111b
Esta separación de ámbitos es lo que hace componibles a los slots. El padre controla el qué —los datos y el marcado del contenido—; el hijo controla el dónde —la posición dentro de su estructura—. Ninguno invade el terreno del otro, y por eso un mismo envoltorio sirve a padres que no comparten ni una sola variable.
Un error de principiante es esperar que el componente hijo acceda a los datos del contenido que recibe —leer el texto de un <p> proyectado, contar sus elementos— a través de Astro.props o de alguna variable. No existe tal cosa: el slot entrega marcado ya resuelto, no valores que el hijo pueda inspeccionar. Si de verdad necesitas que el hijo examine lo proyectado, hay una vía deliberada y avanzada, Astro.slots.render, que verás al cerrar el nivel; para todo lo demás, el hijo solo coloca, nunca lee.
Como todo en Astro, el contenido proyectado se resuelve en el servidor —en build o en cada petición—, no de forma reactiva en el navegador. El hueco se rellena una vez, con el marcado que el padre tenía en ese momento, y se envía ya cosido en el HTML. No existe un slot que reaccione a cambios en vivo: la proyección es una costura del render, fija desde que la página se genera. Por eso componer con slots no arrastra ningún coste de JavaScript en el cliente.
Este mismo principio, escalado hasta la raíz del árbol, es lo que hará posibles los layouts: un componente que envuelve páginas enteras con su <slot /> y les presta cabecera, pie y documento. Lo verás en detalle más adelante; por ahora basta con reconocer que un layout no es más que el caso mayor de esta misma idea, una envoltura con un hueco.
Un componente con props pregunta qué datos le das; un componente con slot pregunta algo más humilde y más poderoso: dónde quieres que ponga lo que traigas. Ese giro es una inversión de control en toda regla. La pieza deja de exigir conocer a sus contenidos y pasa a ofrecer una forma —una envoltura, una posición, un contexto— que se llenará con lo que sea, decidido por quien la usa y no por quien la escribió. Ahí está la raíz de por qué la composición escala donde la configuración se atasca. Un componente que intenta prever cada variante mediante props crece en una maraña de banderas booleanas y ramas condicionales, porque cada nuevo caso obliga a tocar su interior. Un componente que expone un slot no necesita prever nada: delega la variabilidad a su frontera y permanece estable por dentro. La interface Props responde a la pregunta qué necesito saber; el <slot /> responde a la contraria, qué prefiero no saber. Diseñar bien es repartir con criterio entre ambas. Lo que el componente debe interpretar —un estado, una opción, un identificador— entra por props, tipado y vigilado. Lo que el componente solo debe alojar —un titular, un cuerpo, un pie que hoy no imaginas— entra por slot, libre y sin contrato de forma. Cuando esa línea se traza bien, las piezas se anidan como muñecas rusas: cada capa aporta estructura y cede contenido, y el árbol entero se compone sin que ninguna capa conozca a las que envuelve. La ignorancia deliberada, aquí, es la fuente de la reutilización.
- Crea
Panel.astrocon una envoltura<section class="panel">que contenga un único<slot />. - Úsalo desde una página pasándole como hijos un
<h2>y un<p>; comprueba en el HTML que ambos aparecen dentro de la sección, envueltos por su estructura. - Declara una constante en la página y úsala dentro del contenido hijo con
{constante}; confirma que se resuelve con el valor del padre y no con nada del hijo. - Elimina el
<slot />dePanely observa que el contenido hijo desaparece sin error: la prueba viva de que sin marcador no hay proyección.