Skip to content

Angular Content Projection Explained — Building Flexible Reusable Components

DodaTech Updated 2026-06-28 6 min read

In this tutorial, you will learn about Angular Content Projection Explained. We cover key concepts, practical examples, and best practices to help you master this topic.

Angular content projection lets you pass HTML content into a component, making your components flexible and reusable by accepting dynamic children from the parent.

What You'll Learn

  • What content projection is and why it matters
  • How to use ng-content for single-slot projection
  • How to use multiple slots with the select attribute
  • How to access projected content programmatically
  • How conditional projection works

Why It Matters

Content projection is the Angular equivalent of React's children prop or Vue's slots. It lets you build wrapper components like cards, dialogs, and panels that accept arbitrary content without knowing what that content is.

Real-World Use

The Durga Antivirus Pro dashboard has a CardComponent that projects a title, body, and action buttons into different slots. The same card component is used for threat summaries, scan results, and system status — each with different projected content.

flowchart TD
    A[Parent Template] -->|Projected Content| B[Child Component]
    B --> C[ng-content Slot 1]
    B --> D[ng-content Slot 2]
    C --> E[Rendered DOM]
    D --> E
    style B fill:#f97316,color:#fff

Single Slot Projection

The simplest form projects all content into one location:

import { Component } from "@angular/core";

@Component({
  selector: "app-panel",
  standalone: true,
  template: `
    <div class="panel">
      <div class="panel-header">
        <ng-content select="[panel-title]"></ng-content>
      </div>
      <div class="panel-body">
        <ng-content></ng-content>
      </div>
    </div>
  `,
  styles: [`
    .panel { border: 1px solid #ddd; border-radius: 8px; padding: 16px; }
    .panel-header { border-bottom: 1px solid #eee; margin-bottom: 12px; padding-bottom: 8px; }
  `]
})
export class PanelComponent {}

Usage:

@Component({
  selector: "app-user-panel",
  standalone: true,
  imports: [PanelComponent],
  template: `
    <app-panel>
      <h3 panel-title>User Profile</h3>
      <p>This is the body content of the panel.</p>
      <button>Edit Profile</button>
    </app-panel>
  `
})
export class UserPanelComponent {}

Expected output: A styled panel with "User Profile" in the header and the body content below.

ng-content with the select attribute matches elements by CSS selector. The first ng-content captures elements with [panel-title] attribute, and the second captures everything else (no select).

Multiple Slot Projection

Define named slots with the select attribute:

import { Component } from "@angular/core";

@Component({
  selector: "app-modal",
  standalone: true,
  template: `
    <div class="modal-overlay">
      <div class="modal-content">
        <div class="modal-header">
          <ng-content select="[modal-title]"></ng-content>
          <ng-content select="[modal-close]"></ng-content>
        </div>
        <div class="modal-body">
          <ng-content select="[modal-body]"></ng-content>
        </div>
        <div class="modal-footer">
          <ng-content select="[modal-footer]"></ng-content>
        </div>
      </div>
    </div>
  `,
  styles: [`
    .modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; }
    .modal-content { background: white; border-radius: 12px; min-width: 400px; max-width: 600px; }
    .modal-header { padding: 16px; border-bottom: 1px solid #eee; display: flex; justify-content: space-between; }
    .modal-body { padding: 16px; }
    .modal-footer { padding: 16px; border-top: 1px solid #eee; display: flex; justify-content: flex-end; gap: 8px; }
  `]
})
export class ModalComponent {}

Usage with multiple slots:

<app-modal>
  <h2 modal-title>Confirm Deletion</h2>
  <button modal-close (click)="close()">X</button>
  <p modal-body>Are you sure you want to delete this item? This action cannot be undone.</p>
  <div modal-footer>
    <button (click)="confirm()">Delete</button>
    <button (click)="close()">Cancel</button>
  </div>
</app-modal>

Expected output: A modal dialog with header, body, and footer sections, each styled according to the modal component's template.

The select attribute accepts standard CSS selectors: element selectors (footer), class selectors (.my-class), and attribute selectors ([footer]). For class selectors, the projected content must include the class.

Conditional Projection with ngProjectAs

Use ngProjectAs to project an element into a specific slot without modifying its structure:

@Component({
  selector: "app-list-item",
  standalone: true,
  template: `
    <div class="list-item">
      <ng-content select="[item-icon]"></ng-content>
      <div class="item-content">
        <ng-content select="[item-title]"></ng-content>
        <ng-content select="[item-description]"></ng-content>
      </div>
      <ng-content select="[item-actions]"></ng-content>
    </div>
  `
})
export class ListItemComponent {}

Usage with ngProjectAs preserves the original element:

<app-list-item>
  <span item-icon>ICON</span>
  <h4 item-title>Task Name</h4>
  <p item-description>This is a task description</p>
  <div item-actions>
    <button>Edit</button>
    <button>Delete</button>
  </div>
</app-list-item>

Expected output: A list item with icon, title, description, and action buttons arranged horizontally.

ngProjectAs is useful when you wrap projected content in container elements but want the projection to match the slot selector at the same container level.

Accessing Projected Content

Use @ContentChild or @ContentChildren to access projected content programmatically:

import { Component, ContentChild, ElementRef, AfterContentInit } from "@angular/core";

@Component({
  selector: "app-tabs",
  standalone: true,
  template: `
    <div class="tabs">
      <div class="tab-list">
        <button *ngFor="let tab of tabs; let i = index"
          (click)="selectTab(i)"
          [style.font-weight]="selectedIndex === i ? 'bold' : 'normal'">
          {{ tab.title }}
        </button>
      </div>
      <div class="tab-content">
        <ng-content></ng-content>
      </div>
    </div>
  `
})
export class TabsComponent implements AfterContentInit {
  @ContentChildren(TabComponent) tabs!: TabComponent[];
  selectedIndex = 0;

  ngAfterContentInit() {
    console.log("Number of tabs:", this.tabs.length);
  }

  selectTab(index: number) {
    this.selectedIndex = index;
  }
}

Expected output: A tab component that counts its tab children and controls which tab is active.

@ContentChildren queries elements projected via ng-content. It is available in ngAfterContentInit lifecycle hook, not earlier, because the projected content is resolved after initialization.

Common Mistakes

  1. Assuming ng-content renders its own contentng-content is a placeholder. The content belongs to the parent component, not the child. Styles in the child may not apply to projected content.

  2. Using multiple ng-content without select — Multiple ng-content elements without select will render the projected content multiple times. Only the first one renders; the others are ignored.

  3. Forgetting that select uses CSS selectorsselect="header" matches <header> elements, not elements with header attribute. Use select="[header]" for attribute matching.

  4. Accessing ContentChild before AfterContentInit — Content children are not available in ngOnInit. Access them in ngAfterContentInit.

  5. Projecting structural directives into ng-content — A component with *ngIf wrapping the projected content may not project correctly. Use <ng-container> to preserve projection.

Practice Questions

  1. What does ng-content do? It is a placeholder that renders content projected from the parent component.

  2. How do you create multiple projection slots? Use the select attribute on ng-content with CSS selectors to route content into specific slots.

  3. What is the difference between @ViewChild and @ContentChild? @ViewChild queries the component's own template. @ContentChild queries projected content from the parent.

  4. Can you project content conditionally? Yes, wrap ng-content in *ngIf to conditionally render the slot.

  5. What is ngProjectAs? An attribute that lets you project an element as a different selector without changing the element itself.

Challenge

Build a StepperComponent with multiple steps. Each step is projected as <app-step title="Step 1">. Use @ContentChildren to collect all steps. Show a navigation bar with step titles and navigate between steps by showing only the active step's projected content.

FAQ

Does ng-content create a new component?

No, ng-content simply transcludes existing content. It does not create new component instances.

Can I project a component into ng-content?

Yes, component tags are valid HTML as far as ng-content is concerned. The parent component imports the child component.

How do I style projected content?

Styles defined in the parent component apply. The child component styles may not penetrate unless the projection respects view Encapsulation.

What is the difference between ng-content and a template outlet?

ng-content projects existing DOM. Template outlets render a template reference in a specific context.

Can I use ng-content in a directive?

No, ng-content is only valid in component templates.

Mini Project

Build a DataTableComponent that uses content projection for columns. Each column is a <ng-template> projected with *appCol directive. The table component projects header cells and body cells from the column templates. Support sorting by clicking column headers. Use @ContentChildren to access column definitions and render them in the table.

What's Next

Continue with dynamic components and animations:

Angular Dynamic Components, Angular Animations, Angular Components

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro