---
title: "How to Use ng-template, ng-container, and ng-content in Angular (Complete Guide)"
date: "2023-04-13"
slug: "how-to-get-and-use-ng-template-ng-container-and-ng-content"
author: "Dany Paredes"
canonical: "https://danywalls.com/how-to-get-and-use-ng-template-ng-container-and-ng-content"
description: "Learn the exact differences between ng-template, ng-container, and ng-content in Angular, how to pass context with ngTemplateOutlet, multi-slot projection, and how they evolve with modern @if, @for, and @defer control flow."
---


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](#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.

| 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

```html
<!-- 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:

```typescript
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

```html
<!-- ❌ 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`

```html
<!-- ✅ 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:

```html
<!-- 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`

```html
<!-- 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:

```html
@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:

```typescript
// 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:

```html
<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:

```typescript
// 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:

```html
<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:

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](/learn-route-parameters-in-angular-with-example) and [Combine Async Pipes in Angular](/combine-async-pipes-in-angular-how-to-avoid-multiple-pipes)!

