How to Use ng-template, ng-container, and ng-content in Angular (Complete Guide)

When building UI components in Angular, three built-in template tools frequently cause confusion: ng-template, ng-container, and ng-content.
They all start with ng-, they all help build dynamic layouts, but each one solves a fundamentally different architectural problem.
⚡ Already using Modern Angular (v17+)? Jump directly to Modern Angular: How Built-in Control Flow (@if, @for, @defer) Changes Everything to see the new syntax and where these tools still apply today.
In this guide, I will share the mental model for each directive, real-world practical code examples with parameter passing and multi-slot projection, and how they fit into modern Angular with @if, @for, @let, and @defer.
Quick Mental Model: The 3 Core Tools
Here is the mental shortcut I use:
ng-template: "The Blueprint". HTML that Angular holds in memory but does not render until you tell it where and when (viangTemplateOutletor structural directives).ng-container: "The Ghost Wrapper". A logical grouping element that does not create extra DOM nodes, preserving clean CSS Flexbox and Grid layouts.ng-content: "The Slot / Projection Portal". Injects and projects markup from a parent component into designated slots in a child component.
| Tool | Mental Model | DOM Output | Primary Purpose |
|---|---|---|---|
ng-template | 📐 The Blueprint | ❌ None | Inert template instantiated on demand (ngTemplateOutlet) |
ng-container | 👻 The Ghost Wrapper | ❌ None | Invisible grouping element & host for dynamic templates |
ng-content | 🚪 The Projection Portal | ✅ Yes | Injects parent markup into designated child slots |
Let's look at each one in detail, starting with ng-template.
1. ng-template: Defining Reusable and Dynamic Blueprints 📐
By default, any markup placed inside <ng-template> is inert—the browser will never render it directly to the screen. It only appears when instantiated dynamically.
Basic Template Outlet Example
<!-- 1. Define the blueprint in your template -->
<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 it dynamically using ngTemplateOutlet -->
<div class="card">
<ng-container *ngTemplateOutlet="loadingSpinner"></ng-container>
</div>Advanced: Passing Data Context with ngTemplateOutletContext
One of the most powerful features of ng-template is the ability to pass data into the template using $implicit and named context keys:
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-user-card',
standalone: true,
imports: [CommonModule],
template: `
<!-- Template definition with context variables -->
<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>
<!-- Rendering the template with specific data -->
<ng-container
*ngTemplateOutlet="userBadge; context: { user: currentUser, role: 'Lead Architect' }"
></ng-container>
`
})
export class UserCardComponent {
currentUser = { name: 'Dany Paredes', id: '101' };
}Note: The
$implicitkey binds automatically tolet-item, while specific named keys bind tolet-var="keyName".
Now that we know how to define dynamic templates, let's see how ng-container prevents DOM layout issues.
2. ng-container: Clean Grouping Without Extra DOM Nodes 👻
When building responsive layouts with CSS Flexbox or CSS Grid, adding unnecessary <div> wrappers can completely break flex alignment, gap spacing, and grid column placement.
ng-container acts as a purely logical grouping tag. In the browser DOM, it is replaced by an invisible HTML comment marker.
The Problem: Unwanted <div> Tag Pollution
<!-- ❌ BAD: Adds an unnecessary <div> to the DOM that breaks 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>The Solution: Using ng-container
<!-- ✅ GOOD: No extra DOM elements are injected into the grid layout -->
<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>Now let's look at how modern Angular syntax simplifies this even further.
3. Modern Angular: How Built-in Control Flow (@if, @for, @defer) Changes Everything ⚡
If you are working on Angular 17, 18, 19, or newer, the new built-in control flow dramatically simplifies conditional rendering and loops.
Here is a side-by-side comparison of legacy syntax versus the modern approach:
1. Conditional Rendering: *ngIf vs @if
In legacy Angular, you needed <ng-container *ngIf="..."> to avoid extra DOM wrappers. In modern Angular, @if natively renders zero extra DOM nodes:
<!-- Legacy Angular (v2 - v16) -->
<ng-container *ngIf="isLoggedIn; else guestBlock">
<app-user-dashboard />
</ng-container>
<ng-template #guestBlock>
<app-login-prompt />
</ng-template>
<!-- ✨ Modern Angular (v17+) -->
@if (isLoggedIn) {
<app-user-dashboard />
} @else {
<app-login-prompt />
}2. Lists & Loops: *ngFor vs @for
<!-- Legacy Angular -->
<ng-container *ngFor="let product of products; trackBy: trackById">
<app-product-card [product]="product" />
</ng-container>
<!-- ✨ Modern Angular -->
@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. Local Template Variables with @let (Angular 18+)
Instead of relying on *ngIf="stream$ | async as data" on an ng-container, Angular 18+ provides the @let declaration:
@let user = currentUser();
@let role = userRole();
<div class="user-badge">
<h3>{{ user.name }}</h3>
<span>{{ role }}</span>
</div>Do We Still Need ng-template and ng-container in Modern Angular?
Yes! While @if and @for eliminate the need for ng-container in basic conditionals, they remain essential for:
ng-template: Defining dynamic templates for modals, dropdown renderers, customizable data table cells, and template reference variables (TemplateRef).ng-container: Serving as the invisible host for*ngTemplateOutletwhen rendering dynamicng-templateblueprints.@defer: Angular's built-in@defer (on viewport) { ... } @placeholder { ... } @loading { ... }mechanism actually creates dynamic deferred template fragments behind the scenes.
Now let's examine content projection using ng-content.
4. ng-content: Content Projection and Multi-Slot Layouts 🚪
ng-content is Angular's mechanism for content projection (similar to <slot> in Web Components and Vue). It allows a parent component to pass custom markup directly into a child component's template.
Single-Slot Content Projection
Consider a reusable alert banner:
// 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()">
<!-- Injected parent markup lands here -->
<ng-content></ng-content>
</div>
`
})
export class AlertComponent {
readonly alertStyle = input('bg-blue-50 border-blue-200 text-blue-900');
}Parent component usage:
<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>Advanced: Multi-Slot Projection with select
For components with distinct layout sections (like a modal with a header, body, and footer actions), use the select attribute with CSS selectors:
// modal.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-modal',
standalone: true,
template: `
<div class="modal-backdrop">
<div class="modal-dialog">
<!-- 1. Header Slot -->
<header class="modal-header border-b pb-3">
<ng-content select="[modal-header]"></ng-content>
</header>
<!-- 2. Main Body Slot -->
<main class="modal-body py-4">
<ng-content select="[modal-body]"></ng-content>
</main>
<!-- 3. Footer Actions Slot -->
<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 {}Usage in the parent component:
<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>Comparison Matrix: When to Use Which Tool
| Directive | Creates DOM Node? | Renders Immediately? | Best Use Case |
|---|---|---|---|
ng-template | ❌ No | ❌ No (Instantiated on demand) | Dynamic templates, table cell renderers, dialog content |
ng-container | ❌ No | ✅ Yes (If condition passes) | Grouping elements, applying *ngTemplateOutlet |
ng-content | ❌ No | ✅ Yes (When parent passes markup) | Reusable components, multi-slot modals, UI design systems |
@if / @for | ❌ No | ✅ Yes | Built-in control flow for conditionals and loops |
Recap 🛠️
Here is how to choose the right tool for your Angular templates:
- Use
@if,@for, and@switchfor standard loops and conditionals (nong-containerneeded). - Use
ng-templatewhen you need reusable template blueprints with dynamic data contexts (ngTemplateOutlet) or custom component templates. - Use
ng-containeras an invisible mounting point for*ngTemplateOutlet. - Use
ng-contentwhen building design-system components that accept custom child markup via slots.
For more hands-on Angular tutorials, check out my articles on How to Use Route Parameters in Angular with Signals and Combine Async Pipes in Angular!
Modern frontend development — Angular, React, TypeScript, accessibility, and performance.
Frequently Asked Questions
What is the difference between ng-template, ng-container, and ng-content in Angular?
ng-template defines inert template fragments that are not rendered in the DOM until explicitly instantiated with ngTemplateOutlet or structural directives. ng-container is a grouping element that does not create extra HTML DOM nodes. ng-content is used for content projection (transclusion), allowing parent components to project custom markup into designated slots in a child component.
Does ng-container create a real HTML element in the browser DOM?
No. ng-container is a purely logical grouping tag in Angular. When compiled and rendered in the browser, it is completely invisible and generates an HTML comment marker instead of a div or span, preventing broken CSS flexbox and grid layouts.
How does modern Angular built-in control flow (@if, @for, @defer) change how we use ng-container?
Built-in control flow (@if, @for, @switch) eliminates the need for ng-container just to apply conditional rendering without wrapper divs. However, ng-container remains essential as an invisible host for *ngTemplateOutlet, and ng-template is still required for dynamic template blueprints, table cell templates, and modal dialogs.
How do you pass context variables into an ng-template with ngTemplateOutletContext?
You pass an object to [ngTemplateOutletContext]='{ $implicit: item, index: i }'. Inside the ng-template, you bind the default implicit variable using 'let-item' and named variables using 'let-idx=index'.
Related Articles
How to Use Route and Query Parameters in Angular (with Signals & Inputs)
Learn how to pass and read route parameters, query parameters, and matrix parameters in Angular using Signals, withComponentInputBinding, and ActivatedRoute.
Combine Async Pipes in Angular: From combineLatest to Signals and @let
Learn how to avoid multiple async pipes and duplicate subscriptions in Angular templates using combineLatest, modern Signals (toSignal), and the @let template syntax.
Angular's 'exportAs' Feature: A Practical Guide to Sharing Component State
In Angular when we build components with public properties or the state to share with others, some options come into our heads to solve it: Emit an event using the new value of the component state. Inject service with Subject to share the state. Inj...
Creating Dynamic Forms in Angular: A Step-by-Step Guide
Learn how to build data-driven, typed dynamic forms in modern Angular from configuration models using Reactive Forms, Control Flow (@switch, @for), and validation.
Share this article
If you found this guide helpful, consider sharing it with your team or fellow developers.
Real Software. Real Lessons.
I share the lessons I learned the hard way, so you can either avoid them or be ready when they happen.
Join 13,800+ developers and readers.
No spam ever. Unsubscribe at any time.