Angular Content Projection Explained — Building Flexible Reusable Components
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
Assuming ng-content renders its own content —
ng-contentis a placeholder. The content belongs to the parent component, not the child. Styles in the child may not apply to projected content.Using multiple ng-content without select — Multiple
ng-contentelements withoutselectwill render the projected content multiple times. Only the first one renders; the others are ignored.Forgetting that select uses CSS selectors —
select="header"matches<header>elements, not elements withheaderattribute. Useselect="[header]"for attribute matching.Accessing ContentChild before AfterContentInit — Content children are not available in
ngOnInit. Access them inngAfterContentInit.Projecting structural directives into ng-content — A component with
*ngIfwrapping the projected content may not project correctly. Use<ng-container>to preserve projection.
Practice Questions
What does
ng-contentdo? It is a placeholder that renders content projected from the parent component.How do you create multiple projection slots? Use the
selectattribute onng-contentwith CSS selectors to route content into specific slots.What is the difference between @ViewChild and @ContentChild?
@ViewChildqueries the component's own template.@ContentChildqueries projected content from the parent.Can you project content conditionally? Yes, wrap
ng-contentin*ngIfto conditionally render the slot.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
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