Frontend
·8 min read·↗

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

Main cover illustration for article: How to Use ng-template, ng-container, and ng-content in Angular (Complete Guide)
Summarize with AI:

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 (via ngTemplateOutlet or 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.
ToolMental ModelDOM OutputPrimary Purpose
ng-template📐 The Blueprint❌ NoneInert template instantiated on demand (ngTemplateOutlet)
ng-container👻 The Ghost Wrapper❌ NoneInvisible grouping element & host for dynamic templates
ng-content🚪 The Projection Portal✅ YesInjects 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 $implicit key binds automatically to let-item, while specific named keys bind to let-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:

  1. ng-template: Defining dynamic templates for modals, dropdown renderers, customizable data table cells, and template reference variables (TemplateRef).
  2. ng-container: Serving as the invisible host for *ngTemplateOutlet when rendering dynamic ng-template blueprints.
  3. @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

DirectiveCreates 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✅ YesBuilt-in control flow for conditionals and loops

Recap 🛠️

Here is how to choose the right tool for your Angular templates:

  1. Use @if, @for, and @switch for standard loops and conditionals (no ng-container needed).
  2. Use ng-template when you need reusable template blueprints with dynamic data contexts (ngTemplateOutlet) or custom component templates.
  3. Use ng-container as an invisible mounting point for *ngTemplateOutlet.
  4. Use ng-content when 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!

Part of the Frontend Series

Modern frontend development — Angular, React, TypeScript, accessibility, and performance.

View Entire Series

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

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.

User avatar
User avatar
User avatar
User avatar
+13K

Join 13,800+ developers and readers.

No spam ever. Unsubscribe at any time.