Content Projection (ng-content)
Content projection lets a component accept markup from its parent and render it inside its own template. Instead of hard-coding everything a component displays, you reserve a slot with ng-content and let consumers fill it with whatever they need. This is the foundation of flexible, composable UI primitives like cards, dialogs, and layout shells — and it is Angular’s answer to what other frameworks call slots or transclusion.
Why content projection matters
Inputs (@Input) are great for passing data, but they fall short when you want to pass arbitrary template content — rich HTML, other components, or even structural directives. A reusable <app-card> should not have to know in advance whether its body contains a paragraph, a form, or a chart. Content projection inverts the relationship: the parent owns the content, and the child owns the layout and styling around it.
The content placed between a component’s opening and closing tags is the projected content. The component decides where that content appears using the <ng-content> element.
Single-slot projection
The simplest form uses a bare <ng-content> that catches everything passed between the host tags.
import { Component } from '@angular/core';
@Component({
selector: 'app-card',
standalone: true,
template: `
<div class="card">
<ng-content />
</div>
`,
styles: `
.card {
border: 1px solid #e2e2e2;
border-radius: 8px;
padding: 1rem;
}
`,
})
export class CardComponent {}
A parent now wraps any markup inside the card:
<app-card>
<h2>Monthly report</h2>
<p>Revenue is up 12% over last month.</p>
</app-card>
The <h2> and <p> are moved into the .card div at runtime. Note that the projected nodes are not duplicated — they are physically relocated into the slot, so directives and component instances inside them keep their identity.
The self-closing
<ng-content />syntax is supported in modern Angular templates (v17+). Older code may use<ng-content></ng-content>; both are equivalent.
Multi-slot projection with select
Most real components need more structure: a header, a body, and a footer that each render in a different place. Add a select attribute to target content by CSS selector. Each <ng-content select="..."> becomes a named slot, and any leftover content falls into a bare <ng-content> (the default slot).
import { Component } from '@angular/core';
@Component({
selector: 'app-panel',
standalone: true,
template: `
<section class="panel">
<header class="panel__head">
<ng-content select="[panel-title]" />
</header>
<div class="panel__body">
<ng-content />
</div>
<footer class="panel__foot">
<ng-content select="panel-actions" />
</footer>
</section>
`,
})
export class PanelComponent {}
The parent labels each chunk so the component knows where to slot it:
<app-panel>
<h3 panel-title>Account settings</h3>
<p>Update your profile and notification preferences below.</p>
<panel-actions>
<button type="button">Cancel</button>
<button type="submit">Save</button>
</panel-actions>
</app-panel>
Here the <h3> matches the attribute selector [panel-title], the <panel-actions> element matches the element selector, and the <p> — matching no specific slot — lands in the default <ng-content>.
Custom element names like
<panel-actions>will log an “unknown element” warning unless Angular recognizes the selector. Prefer attribute selectors (select="[panel-actions]") for projection markers to keep the DOM valid and warning-free.
Supported selectors
select accepts the same CSS selectors Angular uses for component selectors:
| Selector type | Example select value | Matches |
|---|---|---|
| Element | select="header" | <header>...</header> |
| Attribute | select="[card-body]" | <div card-body>...</div> |
| Class | select=".actions" | <div class="actions">...</div> |
| Combined | select="button[primary]" | <button primary>...</button> |
Conditional and repeated projection
Projected content is only rendered if a matching <ng-content> is present in the template. You can wrap a slot in the new control flow to show it conditionally — useful for optional sections.
import { Component, contentChild } from '@angular/core';
@Component({
selector: 'app-alert',
standalone: true,
template: `
<div class="alert">
@if (icon()) {
<span class="alert__icon">
<ng-content select="[alert-icon]" />
</span>
}
<div class="alert__msg">
<ng-content />
</div>
</div>
`,
})
export class AlertComponent {
// Query whether the icon slot was actually filled.
icon = contentChild('[alert-icon]', { read: undefined });
}
A
<ng-content>may only be projected into one location. Angular does not duplicate projected content, so you cannot reuse the same slot in two branches of an@if. If you need that, project the content once and toggle visibility with CSS or restructure into separate slots.
Best practices
- Prefer attribute selectors (
[panel-title]) over custom element names to avoid invalid-DOM warnings and keep markup semantic. - Always provide a bare default
<ng-content>so unmatched content has somewhere to go instead of being silently dropped. - Keep slot contracts small and documented — too many named slots makes a component hard to consume.
- Use
contentChild/contentChildren(signal queries) when the component must react to or read its projected content programmatically. - Remember projection preserves identity: projected components are instantiated once and keep their state, even when moved into a slot.
- Avoid projecting the same
<ng-content>into multiple positions; design separate slots instead.