Cómo usar ng-template, ng-container y ng-content en Angular (Guía completa)

Cuando construyo componentes de interfaz en Angular, veo que tres herramientas de plantilla suelen generar bastante confusión: ng-template, ng-container y ng-content.
Todas empiezan con ng- y todas ayudan a crear layouts dinámicos, pero cada una resuelve un problema de arquitectura completamente distinto.
⚡ ¿Ya usas Angular moderno (v17+)? Salta directamente a Angular moderno: cómo el control flow integrado (@if, @for, @defer) lo cambia todo para ver la nueva sintaxis y en qué casos seguimos usando estas herramientas.
En esta guía, voy a compartir el modelo mental que uso para cada directiva, ejemplos prácticos de código real con paso de parámetros y proyección en múltiples slots, además de cómo encajan en el Angular moderno con @if, @for, @let y @defer.
Modelo mental rápido: las 3 herramientas clave
Este es el atajo mental que utilizo:
ng-template: "El plano" (The Blueprint). HTML que Angular guarda en memoria pero no renderiza hasta que le indicas exactamente dónde y cuándo (a través dengTemplateOutleto directivas estructurales).ng-container: "El contenedor fantasma" (The Ghost Wrapper). Un elemento de agrupación lógica que no crea nodos extra en el DOM, preservando layouts limpios con CSS Flexbox y Grid.ng-content: "El portal de proyección" (The Slot / Projection Portal). Inyecta y proyecta marcado desde un componente padre dentro de slots designados en un componente hijo.
| Herramienta | Modelo mental | Salida en el DOM | Propósito principal |
|---|---|---|---|
ng-template | 📐 El plano | ❌ Ninguno | Plantilla inerte instanciada bajo demanda (ngTemplateOutlet) |
ng-container | 👻 El contenedor fantasma | ❌ Ninguno | Agrupación invisible y host para plantillas dinámicas |
ng-content | 🚪 El portal de proyección | ✅ Sí | Inyecta marcado del padre en slots designados del hijo |
Veamos cada una en detalle, empezando por ng-template.
1. ng-template: definir planos reutilizables y dinámicos 📐
Por defecto, cualquier marcado colocado dentro de <ng-template> es inerte: el navegador nunca lo renderizará directamente en pantalla. Solo aparece cuando lo instanciamos de forma dinámica.
Ejemplo básico con Template Outlet
<!-- 1. Define el plano en tu plantilla -->
<ng-template #loadingSpinner>
<div class="flex items-center gap-2 text-blue-600">
<span class="animate-spin">⏳</span>
<p>Fetching data, please wait...</p>
</div>
</ng-template>
<!-- 2. Renderízalo dinámicamente con ngTemplateOutlet -->
<div class="card">
<ng-container *ngTemplateOutlet="loadingSpinner"></ng-container>
</div>Avanzado: pasar contexto de datos con ngTemplateOutletContext
Una de las características más útiles de ng-template es la capacidad de pasar datos a la plantilla usando $implicit y claves de contexto con nombre:
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-user-card',
standalone: true,
imports: [CommonModule],
template: `
<!-- Definición de la plantilla con variables de contexto -->
<ng-template #userBadge let-user="user" let-role="role">
<div class="p-3 border rounded-lg bg-neutral-50 dark:bg-neutral-900">
<h4 class="font-bold text-neutral-900 dark:text-neutral-100">{{ user.name }}</h4>
<span class="text-xs px-2 py-1 bg-blue-100 text-blue-800 rounded">{{ role }}</span>
</div>
</ng-template>
<!-- Renderizado de la plantilla con datos específicos -->
<ng-container
*ngTemplateOutlet="userBadge; context: { user: currentUser, role: 'Lead Architect' }"
></ng-container>
`
})
export class UserCardComponent {
currentUser = { name: 'Dany Paredes', id: '101' };
}Nota: la clave
$implicitse vincula automáticamente alet-item, mientras que las claves con nombre específico se vinculan alet-var="keyName".
Ahora que ya sabemos cómo definir plantillas dinámicas, veamos cómo ng-container evita problemas de estructura en el DOM.
2. ng-container: agrupación limpia sin nodos DOM adicionales 👻
Al construir layouts responsivos con CSS Flexbox o CSS Grid, agregar wrappers <div> innecesarios puede romper por completo la alineación flex, el espaciado con gap y la distribución de columnas en el grid.
ng-container actúa como una etiqueta de agrupación puramente lógica. En el DOM del navegador, se reemplaza por un comentario HTML invisible.
El problema: contaminación de etiquetas <div> innecesarias
<!-- ❌ MAL: Agrega un <div> innecesario al DOM que rompe CSS Grid -->
<div class="grid grid-cols-3 gap-4">
<div *ngIf="isLoggedIn">
<div class="card">Profile</div>
<div class="card">Settings</div>
</div>
</div>La solución: usar ng-container
<!-- ✅ BIEN: No se inyectan elementos DOM adicionales en el layout del grid -->
<div class="grid grid-cols-3 gap-4">
<ng-container *ngIf="isLoggedIn">
<div class="card">Profile</div>
<div class="card">Settings</div>
</ng-container>
</div>Ahora veamos cómo la sintaxis moderna de Angular simplifica esto todavía más.
3. Angular moderno: cómo el control flow integrado (@if, @for, @defer) lo cambia todo ⚡
Si trabajas con Angular 17, 18, 19 o versiones más recientes, el nuevo control flow integrado simplifica de forma notable el renderizado condicional y los bucles.
Aquí tienes una comparación lado a lado entre la sintaxis tradicional y el enfoque moderno:
1. Renderizado condicional: *ngIf vs @if
En versiones anteriores de Angular, necesitabas <ng-container *ngIf="..."> para evitar wrappers innecesarios en el DOM. En Angular moderno, @if genera de forma nativa cero nodos DOM adicionales:
<!-- Angular tradicional (v2 - v16) -->
<ng-container *ngIf="isLoggedIn; else guestBlock">
<app-user-dashboard />
</ng-container>
<ng-template #guestBlock>
<app-login-prompt />
</ng-template>
<!-- ✨ Angular moderno (v17+) -->
@if (isLoggedIn) {
<app-user-dashboard />
} @else {
<app-login-prompt />
}2. Listas y bucles: *ngFor vs @for
<!-- Angular tradicional -->
<ng-container *ngFor="let product of products; trackBy: trackById">
<app-product-card [product]="product" />
</ng-container>
<!-- ✨ Angular moderno -->
@for (product of products; track product.id) {
<app-product-card [product]="product" />
} @empty {
<p class="text-neutral-500">No products available in this category.</p>
}3. Variables locales de plantilla con @let (Angular 18+)
En lugar de depender de *ngIf="stream$ | async as data" en un ng-container, Angular 18+ introduce la declaración @let:
@let user = currentUser();
@let role = userRole();
<div class="user-badge">
<h3>{{ user.name }}</h3>
<span>{{ role }}</span>
</div>¿Seguimos necesitando ng-template y ng-container en Angular moderno?
¡Sí! Aunque @if y @for eliminan la necesidad de ng-container en condicionales básicos, ambas directivas siguen siendo indispensables para:
ng-template: definir plantillas dinámicas para modales, renderizadores de menús desplegables, celdas configurables en tablas y variables de referencia de plantilla (TemplateRef).ng-container: actuar como host invisible para*ngTemplateOutletcuando renderizamos planos dinámicos deng-template.@defer: el mecanismo integrado@defer (on viewport) { ... } @placeholder { ... } @loading { ... }crea internamente fragmentos diferidos de plantilla tras bambalinas.
Ahora revisemos la proyección de contenido con ng-content.
4. ng-content: proyección de contenido y layouts con múltiples slots 🚪
ng-content es el mecanismo de Angular para la proyección de contenido (similar a <slot> en Web Components y Vue). Permite que un componente padre pase marcado personalizado directamente dentro de la plantilla de un componente hijo.
Proyección de contenido en un solo slot
Veamos el caso de un banner de alerta reutilizable:
// alert.component.ts
import { Component, input } from '@angular/core';
@Component({
selector: 'app-alert',
standalone: true,
template: `
<div class="p-4 rounded-xl border" [class]="alertStyle()">
<!-- El marcado inyectado por el padre se coloca aquí -->
<ng-content></ng-content>
</div>
`
})
export class AlertComponent {
readonly alertStyle = input('bg-blue-50 border-blue-200 text-blue-900');
}Uso en el componente padre:
<app-alert>
<h4 class="font-bold">Deployment Complete!</h4>
<p class="text-sm">Your application has been deployed to Google Cloud Run.</p>
</app-alert>Avanzado: proyección en múltiples slots con select
Para componentes que tienen secciones de layout diferenciadas (como un modal con cabecera, cuerpo y acciones en el pie), usamos el atributo select con selectores CSS:
// modal.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-modal',
standalone: true,
template: `
<div class="modal-backdrop">
<div class="modal-dialog">
<!-- 1. Slot de cabecera -->
<header class="modal-header border-b pb-3">
<ng-content select="[modal-header]"></ng-content>
</header>
<!-- 2. Slot del cuerpo principal -->
<main class="modal-body py-4">
<ng-content select="[modal-body]"></ng-content>
</main>
<!-- 3. Slot de acciones en el pie -->
<footer class="modal-footer border-t pt-3 flex justify-end gap-2">
<ng-content select="[modal-footer]"></ng-content>
</footer>
</div>
</div>
`
})
export class ModalComponent {}Uso en el componente padre:
<app-modal>
<h3 modal-header class="text-xl font-bold">Confirm Deletion</h3>
<p modal-body>Are you sure you want to delete this item? This action cannot be undone.</p>
<div modal-footer>
<button class="btn-cancel">Cancel</button>
<button class="btn-danger">Delete</button>
</div>
</app-modal>Matriz comparativa: cuándo usar cada herramienta
| Directiva | ¿Crea nodo en el DOM? | ¿Se renderiza de inmediato? | Mejor caso de uso |
|---|---|---|---|
ng-template | ❌ No | ❌ No (se instancia bajo demanda) | Plantillas dinámicas, renderizado de celdas de tabla, modales |
ng-container | ❌ No | ✅ Sí (si cumple la condición) | Agrupar elementos, aplicar *ngTemplateOutlet |
ng-content | ❌ No | ✅ Sí (cuando el padre pasa marcado) | Componentes reutilizables, modales con múltiples slots, sistemas de diseño |
@if / @for | ❌ No | ✅ Sí | Control flow integrado para condicionales y bucles |
Resumen 🛠️
Así es como elijo la herramienta adecuada para las plantillas en Angular:
- Uso
@if,@fory@switchpara condicionales y bucles estándar (sin necesidad deng-container). - Uso
ng-templatecuando necesito planos de plantilla reutilizables con contexto de datos dinámico (ngTemplateOutlet) o plantillas personalizadas para componentes. - Uso
ng-containercomo punto de anclaje invisible para*ngTemplateOutlet. - Uso
ng-contental crear componentes de sistemas de diseño que reciben marcado personalizado desde el componente padre mediante slots.
Si quieres ver más tutoriales prácticos de Angular, te invito a leer mis artículos sobre cómo usar parámetros de ruta en Angular con Signals y cómo combinar async pipes en Angular.
Modern frontend development — Angular, React, TypeScript, accessibility, and performance.
Preguntas Frecuentes
¿Cuál es la diferencia entre ng-template, ng-container y ng-content en Angular?
ng-template define fragmentos de plantilla inertes que no se renderizan en el DOM hasta que se instancian explícitamente con ngTemplateOutlet o directivas estructurales. ng-container es un elemento de agrupación que no crea nodos HTML adicionales en el DOM. ng-content se usa para proyectar contenido (transclusión), permitiendo que los componentes padre proyecten marcado personalizado en slots designados dentro de un componente hijo.
¿ng-container crea un elemento HTML real en el DOM del navegador?
No. ng-container es una etiqueta de agrupación puramente lógica en Angular. Cuando se compila y renderiza en el navegador, es completamente invisible y genera un comentario HTML en lugar de un div o span, evitando problemas de diseño en CSS Flexbox y Grid.
¿Cómo cambia el control flow integrado de Angular (@if, @for, @defer) la forma en que usamos ng-container?
El control flow integrado (@if, @for, @switch) elimina la necesidad de usar ng-container solo para renderizado condicional sin divs adicionales. Sin embargo, ng-container sigue siendo fundamental como contenedor invisible para *ngTemplateOutlet, y ng-template todavía se necesita para plantillas dinámicas, celdas de tablas y diálogos modales.
¿Cómo se pasan variables de contexto a un ng-template con ngTemplateOutletContext?
Pasas un objeto a [ngTemplateOutletContext]='{ $implicit: item, index: i }'. Dentro del ng-template, enlazas la variable implícita por defecto usando 'let-item' y las variables con nombre usando 'let-idx=index'.
Artículos Relacionados
Cómo usar parámetros de ruta y query params en Angular (con Signals e Inputs)
Aprende a pasar y leer parámetros de ruta, query params y parámetros de matriz en Angular usando Signals, withComponentInputBinding y ActivatedRoute.
Cómo combinar Async Pipes en Angular: De combineLatest a Signals y @let
Aprende cómo evitar múltiples async pipes y suscripciones duplicadas en plantillas de Angular usando combineLatest, Signals modernas (toSignal) y la sintaxis @let.
La propiedad 'exportAs' en Angular: Guía práctica para compartir el estado de componentes
En Angular, cuando creamos componentes con propiedades públicas o estado para compartir con otros, existen varias opciones: emitir eventos, inyectar un servicio con Subject o usar la propiedad exportAs en el template.
Cómo crear formularios dinámicos en Angular: Guía paso a paso
Aprende a construir formularios dinámicos tipados y basados en datos en Angular moderno usando Formularios Reactivos, Control Flow (@switch, @for) y validaciones.
Compartir este artículo
Si esta guía te resultó útil, compártela con tu equipo o con otros desarrolladores.
Software real. Lecciones reales.
Comparto las lecciones que aprendí por las malas, para que puedas evitarlas o estar preparado cuando ocurran.
Únete a 13,800+ desarrolladores y lectores.
Cero spam. Cancela tu suscripción en cualquier momento.