Content Projection in Angular
Content projection is a pattern that lets you insert, or project, content you want to use inside another component. It is Angular's equivalent of slots in web components or the children prop in React.
Using content projection you can build reusable wrapper components — cards, modals, tabs, accordions — that know nothing about what they will display but control how it is styled and positioned.
Basic Single-Slot Projection
The simplest form uses a single <ng-content> element. Whatever the parent puts between the component's tags gets projected into that slot.
// card.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-card',
standalone: true,
template: `
<div class="card">
<div class="card-body">
<ng-content />
</div>
</div>
`,
styles: [`
.card { border: 1px solid #ddd; border-radius: 8px; padding: 16px; }
`],
})
export class CardComponent {}
<!-- parent template --> <app-card> <h2>Hello World</h2> <p>This paragraph is projected into the card.</p> </app-card>
At runtime Angular replaces <ng-content /> with the <h2> and <p> elements — the card component never needs to know what they are.
Multi-Slot Projection with select
A component can have multiple <ng-content> elements, each targeting different projected content via a CSS selector on the select attribute. Angular matches elements from the parent template against these selectors and routes them into the correct slot.
// panel.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-panel',
standalone: true,
template: `
<section class="panel">
<header class="panel-header">
<ng-content select="[panel-title]" />
</header>
<div class="panel-body">
<ng-content select="[panel-body]" />
</div>
<footer class="panel-footer">
<ng-content select="[panel-footer]" />
</footer>
</section>
`,
})
export class PanelComponent {}
<!-- parent template -->
<app-panel>
<h3 panel-title>User Profile</h3>
<div panel-body>
<p>Name: Jane Doe</p>
<p>Role: Administrator</p>
</div>
<button panel-footer (click)="save()">Save</button>
</app-panel>
Fallback Content
Angular 17+ supports fallback content inside <ng-content>. If the parent provides no matching content, the fallback is rendered instead.
@Component({
selector: 'app-avatar',
standalone: true,
template: `
<div class="avatar">
<ng-content>
<!-- fallback shown when no content is projected -->
<span class="initials">?</span>
</ng-content>
</div>
`,
})
export class AvatarComponent {}
<!-- no content provided — fallback "?" is shown --> <app-avatar /> <!-- content provided — fallback is ignored --> <app-avatar> <img src="profile.jpg" alt="Profile" /> </app-avatar>
ngProjectAs
Sometimes you want to wrap projected content in another element (e.g. for styling) but still have it match a specific select slot. Use the ngProjectAs attribute to make Angular match the wrapper against a different selector.
<!-- Without ngProjectAs the <ng-container> would not match select="button" -->
<app-toolbar>
<ng-container ngProjectAs="button">
<button (click)="doAction()">Action</button>
</ng-container>
</app-toolbar>
Accessing Projected Content with @ContentChild
The host component can query projected content using @ContentChild (single) or @ContentChildren (multiple). This is useful when you need to read properties or call methods on projected elements.
import { Component, ContentChild, AfterContentInit } from '@angular/core';
import { TabComponent } from './tab.component';
@Component({
selector: 'app-tabs',
standalone: true,
template: `
<div class="tabs">
<ng-content />
</div>
`,
})
export class TabsComponent implements AfterContentInit {
@ContentChild(TabComponent) firstTab!: TabComponent;
ngAfterContentInit() {
// projected content is available here
console.log('First tab label:', this.firstTab.label);
}
}
Content Projection vs Input Properties
Aspect | Content Projection | @Input Property |
|---|---|---|
What is passed | DOM nodes / components | Data (strings, objects, arrays) |
Syntax in parent | Between component tags | As attribute binding [prop]="value" |
Good for | Structural flexibility, rich markup | Configuring component behaviour |
Parent keeps ownership | Yes — parent template owns nodes | No — component owns the value |
Real-World Example: Modal Component
// modal.component.ts
import { Component, Input, Output, EventEmitter } from '@angular/core';
@Component({
selector: 'app-modal',
standalone: true,
template: `
@if (isOpen) {
<div class="overlay" (click)="close()">
<div class="modal" (click)="$event.stopPropagation()">
<div class="modal-header">
<ng-content select="[modal-header]">
<span>Modal</span>
</ng-content>
<button class="close-btn" (click)="close()">x</button>
</div>
<div class="modal-body">
<ng-content select="[modal-body]" />
</div>
<div class="modal-footer">
<ng-content select="[modal-footer]">
<button (click)="close()">Close</button>
</ng-content>
</div>
</div>
</div>
}
`,
})
export class ModalComponent {
@Input() isOpen = false;
@Output() closed = new EventEmitter<void>();
close() {
this.closed.emit();
}
}
<!-- usage -->
<app-modal [isOpen]="showModal" (closed)="showModal = false">
<h2 modal-header>Confirm Delete</h2>
<p modal-body>
Are you sure you want to delete this item? This cannot be undone.
</p>
<div modal-footer>
<button (click)="showModal = false">Cancel</button>
<button class="danger" (click)="confirmDelete()">Delete</button>
</div>
</app-modal>
Key Takeaways
Use <ng-content /> for single-slot projection — anything between component tags is projected.
Use select="[attribute]" or select="element" for multi-slot projection.
Angular 17+ supports fallback content inside <ng-content> tags.
ngProjectAs lets a wrapper element pretend to be a different selector.
@ContentChild / @ContentChildren query projected nodes from the host component.
Content projection is ideal for layout/container components; @Input is better for passing data.