Angular ng-template and ng-container: Template Fragments, Logical Grouping, ngTemplateOutlet [2026]

Link copied
Angular ng-template and ng-container: Template Fragments, Logical Grouping, ngTemplateOutlet [2026]

Angular ng-template and ng-container: Template Fragments, Logical Grouping, ngTemplateOutlet [2026]

Two of Angular's most-under-explained features sit in plain sight: <ng-template> and <ng-container>. They look like HTML elements but render no DOM by themselves. They are the building blocks of every structural directive Angular ships with, and they unlock template patterns that are otherwise impossible. This lesson explains exactly what each one does and when to reach for it.

This is lesson 2.10 of the Angular Tutorial. Lesson 2.9 showed how structural directives consume <ng-template>; this lesson explains the template element itself.

The one-paragraph summary #

<ng-template> is a declared but not yet rendered chunk of HTML. Angular sees it, parses it, and waits — nothing appears in the DOM until something calls createEmbeddedView() on its TemplateRef. <ng-container> is the opposite — a wrapper that renders its children but no element of its own, useful when you need to group elements logically without adding an extra <div> to the page.

<ng-template> — the deferred template #

<ng-template #greeting>
  <p>Hello, world!</p>
</ng-template>

That renders nothing. The <ng-template> element vanishes; the <p> inside is parsed but not inserted. Open browser DevTools and you will see an empty placeholder comment where it was.

To render the template, something needs to grab the TemplateRef and call createEmbeddedView on a ViewContainerRef. There are three common ways:

1. A structural directive #

Every *ngIf / *ngFor / *appUnless / @if (under the hood) uses <ng-template>. The * prefix is sugar:

<!-- This: -->
<p *ngIf="loggedIn">Welcome back!</p>

<!-- Desugars to: -->
<ng-template [ngIf]="loggedIn">
  <p>Welcome back!</p>
</ng-template>

The *ngIf directive receives the TemplateRef of its host <ng-template> and decides when to render it. Same story for *ngFor (renders the template once per item) and any custom structural directive.

2. ngTemplateOutlet #

<ng-template #empty>
  <p>Nothing here yet.</p>
</ng-template>

<div *ngTemplateOutlet="empty"></div>

ngTemplateOutlet renders a referenced template wherever you place it. Useful for shared template fragments — a single <ng-template #empty> reused in multiple spots.

3. Manual createEmbeddedView in TypeScript #

For advanced cases (modals, dynamic positioning, etc.):

@Component({
  template: `
    <ng-template #content>
      <p>{{ message() }}</p>
    </ng-template>
  `,
})
export class ModalComponent {
  private vcr = inject(ViewContainerRef);
  message = signal('Hello');

  show() {
    const tpl = inject(ViewChild(content));   // or a viewChild signal
    const view = this.vcr.createEmbeddedView(tpl);
  }
}

Rare in everyday code; useful for libraries that programmatically render templates.

<ng-template> with context #

Templates can receive context — an object passed in when rendering:

<ng-template #userRow let-user let-i="index">
  <li>{{ i + 1 }}. {{ user.name }}</li>
</ng-template>

<ul>
  @for (u of users(); track u.id; let i = $index) {
    <ng-container *ngTemplateOutlet="userRow; context: { $implicit: u, index: i }"></ng-container>
  }
</ul>

Four mechanics:

  • let-user binds the context's $implicit property to user
  • let-i="index" binds the index property to i
  • context: { $implicit: u, index: i } is the object passed in
  • The template can reuse the same fragment with different data

In 2026 code this pattern is mostly replaced by @for blocks for the iteration case, but it remains useful for reusable template fragments (e.g., a card layout used in three different places with different data).

<ng-container> — the invisible wrapper #

The inverse of <ng-template> — renders its children but adds nothing itself:

<!-- This adds a wrapping <div> -->
<div *ngIf="loggedIn">
  <p>Welcome!</p>
  <button (click)="logout()">Sign out</button>
</div>

<!-- This does NOT add any wrapper -->
<ng-container *ngIf="loggedIn">
  <p>Welcome!</p>
  <button (click)="logout()">Sign out</button>
</ng-container>

The rendered DOM in the second case is exactly the <p> plus the <button> — no <ng-container> element survives. This is the cleanest way to apply a structural directive to a group of siblings without polluting the DOM with wrapper <div>s.

In 2026 the @if and @for blocks made <ng-container> mostly unnecessary for the everyday case:

<!-- Modern equivalent — same DOM, cleaner template -->
@if (loggedIn) {
  <p>Welcome!</p>
  <button (click)="logout()">Sign out</button>
}

But <ng-container> is still the right tool when you need to apply multiple directives to a logical group, or when you need a <ng-template>-driven directive on a sibling group.

When you actually use these in 2026 code #

A realistic survey of usage in modern Angular code:

Use case Solution
Conditional rendering of a single element @if (cond) { ... }
Conditional rendering of a sibling group @if (cond) { <p/><p/> } — no <ng-container> needed
Iterate an array @for (item of items; track item.id) { ... }
Reusable template fragment with context <ng-template> + *ngTemplateOutlet
Inserting a child component dynamically ViewContainerRef.createComponent() (lesson 2.14)
Card pattern with named slots <ng-content select="..."> (lesson 2.6)
Custom structural directive <ng-template> + manual createEmbeddedView

The last three are where <ng-template> earns its keep. The first three are pure @if/@for territory now.

<ng-content> vs <ng-template> vs <ng-container> #

The trio is easy to confuse. The cheat sheet:

Element What it does When to use
<ng-content> Projects content the parent passed in (lesson 2.6) Building reusable wrapper components
<ng-template> Declares but does not render a template Structural directives, shared fragments, dynamic rendering
<ng-container> Renders children with no wrapper element Logical grouping, multi-directive composition

The three are not interchangeable. Pick by what the result must look like in the DOM:

  • Need to insert outside content?<ng-content>
  • Need a placeholder that something else will render?<ng-template>
  • Need an invisible wrapper?<ng-container>

A card component lets the consumer pass a custom footer via template projection:

@Component({
  selector: 'app-card',
  template: `
    <section class="card">
      <ng-content select="[card-header]" />
      <div class="card-body">
        <ng-content />
      </div>
      @if (footer()) {
        <footer class="card-footer">
          <ng-container *ngTemplateOutlet="footer()!" />
        </footer>
      }
    </section>
  `,
})
export class CardComponent {
  footer = input<TemplateRef<unknown> | null>(null);
}

Usage:

<ng-template #saveFooter>
  <button (click)="save()">Save</button>
  <button (click)="cancel()">Cancel</button>
</ng-template>

<app-card [footer]="saveFooter">
  <h2 card-header>Edit profile</h2>
  <p>...form fields here...</p>
</app-card>

This pattern is the cleanest way for a parent to provide both static slotted content (the header and body, via <ng-content>) and dynamic templated content (the footer, via <ng-template> reference passed as an input). Both ng-elements pull their weight.

Common gotchas #

Symptom Cause Fix
<ng-template> renders nothing That's correct — it's a declaration, not a rendering Use a structural directive, ngTemplateOutlet, or manual createEmbeddedView to render it
let-x binding shows undefined The context object doesn't have an $implicit property Pass { $implicit: value } from the consumer, not just { value }
<ng-container> adds extra DOM You probably wrote <ng-container> instead of <ng-container *ngIf="..."> — without a directive it serves no purpose Either add a structural directive, or drop the <ng-container> entirely
*ngTemplateOutlet says template is not found The template reference variable (#name) is out of scope or misspelled Templates are scoped to their containing template — ensure both the declaration and the outlet are in the same component template
Migration codemod removed all <ng-container *ngIf> wrappers That's intentional — the @if block replaces them Manually re-add <ng-container> only where two directives still need to share a logical group

What's next #

Lesson 2.11 walks styling and ViewEncapsulation — the rules that determine which CSS rules apply where, and the three encapsulation modes. Lesson 2.12 covers host element metadata. Lesson 2.13 is signal queries. With all that under your belt, you have the full component toolkit.

After Module 2 closes, Module 3 dives into the signal system that has appeared throughout these lessons.

Try it yourself #

A tabs component that uses both <ng-template> for the tab content and <ng-container> for the active selection:

import { Component, contentChildren, signal, TemplateRef } from '@angular/core';
import { NgTemplateOutlet } from '@angular/common';

@Component({
  selector: 'app-tabs',
  imports: [NgTemplateOutlet],
  template: `
    <div class="tab-bar">
      @for (t of tabs(); track t.label; let i = $index) {
        <button [class.active]="i === activeIdx()" (click)="activeIdx.set(i)">
          {{ t.label }}
        </button>
      }
    </div>
    <div class="tab-body">
      <ng-container *ngTemplateOutlet="tabs()[activeIdx()].tpl" />
    </div>
  `,
})
export class TabsComponent {
  tabs = input.required<{ label: string; tpl: TemplateRef<unknown> }[]>();
  activeIdx = signal(0);
}

The consumer declares each tab's content as a <ng-template> and passes the references in. The app-tabs component swaps which template is rendered via <ng-container *ngTemplateOutlet>. Zero extra DOM, full template reuse, type-safe.

YouDo I still need if I have @if and @for now?
Claude · used get_best_practicesMostly no, but yes for a few cases: (1) applying *ngTemplateOutlet without a wrapper element, (2) attaching multiple structural directives (rare since you can nest @if inside @for and vice versa now), and (3) some Angular Material patterns that consume TemplateRef via projection. For everyday conditional / loop rendering, @if and @for handle it without the wrapper.

Lesson 2.11 picks up styling and ViewEncapsulation — the three CSS scoping modes.

Up next in Angular

More from this topic

View all Angular articles →

Enjoyed this article?

Get new Angular tutorials delivered. No spam — just code-first articles when they ship.

Leave a Comment

Your email stays private. Required fields are marked *

Leave a Comment

Your email stays private. Required fields are marked *