Content projection: ng-content
Inputs pass data into a component, but sometimes you need to pass a whole piece of markup: a heading with an icon, a form, a list. This is what content projection is for — the <ng-content> tag marks the spot where Angular inserts everything written between the component's opening and closing tags.
A wrapper component
Let's make a card with a shadow that can hold anything:
ng generate component card<!-- src/app/card/card.html -->
<div class="card">
<ng-content />
</div>/* src/app/card/card.css */
.card {
padding: 16px;
border-radius: 8px;
box-shadow: 0 2px 6px rgb(0 0 0 / 0.15);
}The Card class in src/app/card/card.ts stays empty — all the work is in the template.
Using it
The parent imports Card and writes the content inside the tag:
<!-- src/app/app.html -->
<app-card>
<h2>To-do list</h2>
<app-todo-list />
</app-card>// src/app/app.ts
import { Component } from '@angular/core';
import { Card } from './card/card';
import { TodoList } from './todo-list/todo-list';
@Component({
selector: 'app-root',
imports: [Card, TodoList],
templateUrl: './app.html',
styleUrl: './app.css',
})
export class App {}The heading and the to-do list end up inside the box with a shadow — in place of <ng-content />. Without that tag the content simply does not appear on the page.
Multiple slots
The select attribute sets which elements go into a given slot. Its value is a CSS selector:
<!-- src/app/card/card.html -->
<div class="card">
<header class="card-header">
<ng-content select="[card-title]" />
</header>
<ng-content />
</div><!-- src/app/app.html -->
<app-card>
<h2 card-title>To-do list</h2>
<app-todo-list />
</app-card>The element with the card-title attribute goes into the card header, and everything else into the slot without select. The card-title attribute does nothing by itself — it is just a marker for the selector.
How it differs from input()
| Approach | What it passes | Example |
|---|---|---|
input() | Data: strings, numbers, objects | A task for app-todo-item |
ng-content | Ready-made markup and components | The contents of a card |
A component with ng-content does not know what will be put inside it — that is why it suits wrappers: cards, modal windows, tabs.
Common mistake
Expecting an @if around <ng-content /> in the child to prevent the content from being created. Components inside the <app-card> tag are created by the parent, and they are always created, even if the slot is hidden. If a heavy component is not always needed, wrap the component itself in @if — in the parent's template.