Skip to content

Angular Content Projection

Content projection lets a component's consumer supply custom markup that gets rendered inside a predefined slot in the component's template. This lesson covers <ng-content> and multi-slot projection.

What Is Content Projection?

Content projection (similar to "slots" in other frameworks, or children in React) lets a component define a placeholder, <ng-content>, where a parent's markup gets rendered. This makes components like cards, modals, and layout wrappers flexible without hardcoding their inner content.

Without content projection, a card component would have to expose every possible piece of content as a separate @Input(), quickly becoming unwieldy for anything beyond simple text.

// card.component.ts
@Component({
  selector: 'app-card',
  standalone: true,
  template: `
    <div class="card">
      <ng-content></ng-content>
    </div>
  `,
})
export class CardComponent {}

<!-- usage -->
<app-card>
  <h3>Order #1024</h3>
  <p>Shipped on March 3rd.</p>
</app-card>

Everything between <app-card> and </app-card> is projected into the <ng-content> placeholder inside the card's own template.

Content Projection Syntax

<!-- single slot -->
<ng-content></ng-content>

<!-- multi-slot, matched by CSS selector -->
<ng-content select="[card-header]"></ng-content>
<ng-content></ng-content>
<ng-content select="[card-footer]"></ng-content>
  • A single <ng-content> projects everything a parent places inside the component's tags.
  • Adding a select attribute creates a named slot that only matches elements with that CSS selector.
  • An unselected <ng-content> acts as the default slot, catching anything that didn't match a named slot.
  • Slot matching uses CSS selectors: attribute selectors, class selectors, or element/tag selectors.

Content Projection Cheatsheet

Common patterns for projecting content into reusable components.

Pattern Example Purpose
Default slot <ng-content></ng-content> Project all unmatched content
Attribute-selected slot <ng-content select="[card-header]"> Project only elements with that attribute
Class-selected slot <ng-content select=".footer"> Project only elements with that class
Tag-selected slot <ng-content select="app-icon"> Project only a specific component/tag
Fallback content content between the tags (rare) Angular has no native fallback; use *ngIf/@if with @ContentChild

Multi-Slot Content Projection

For components with multiple distinct content regions (like a card with a header, body, and footer), define multiple <ng-content> elements with select attributes to route content into the right place.

// card.component.ts template
<div class="card">
  <header class="card-header">
    <ng-content select="[card-header]"></ng-content>
  </header>
  <div class="card-body">
    <ng-content></ng-content>
  </div>
  <footer class="card-footer">
    <ng-content select="[card-footer]"></ng-content>
  </footer>
</div>

<!-- usage -->
<app-card>
  <h3 card-header>Order #1024</h3>
  <p>Shipped on March 3rd.</p>
  <button card-footer>Track Package</button>
</app-card>

The unmarked <p> falls into the default (unselected) <ng-content> slot in the card body.

Dynamic Projection With ng-template and NgTemplateOutlet

For cases where a parent needs to pass a whole reusable template (potentially with context data), <ng-template> combined with NgTemplateOutlet offers more flexibility than static content projection.

<!-- child component template -->
<ng-container *ngTemplateOutlet="rowTemplate; context: { $implicit: item }"></ng-container>

<!-- parent usage -->
<app-list [items]="items">
  <ng-template #rowTemplate let-item>
    <span>{{ item.name }}</span>
  </ng-template>
</app-list>

When to Use Content Projection vs Inputs

Use @Input() for structured data the component needs to know about (a title string, a price number). Use content projection when the consumer needs to supply arbitrary, possibly complex markup, like a card body, a custom icon, or a list of buttons.

Common Mistakes

  • Trying to project content into a component that has no <ng-content> placeholder in its template.
  • Using overly broad select selectors that accidentally capture more elements than intended.
  • Reaching for many @Input() properties to pass markup piece by piece instead of using content projection.
  • Forgetting that projected content is compiled in the parent's context, not the child's, so it can't reference the child component's private class members.

Key Takeaways

  • <ng-content> projects markup supplied by a parent into a component's own template.
  • Multiple named slots can be created using the select attribute with CSS selectors.
  • <ng-template> with NgTemplateOutlet supports more advanced, context-aware projection.
  • Content projection is best for arbitrary markup; inputs are best for structured data.

Pro Tip

Use attribute selectors (like [card-header]) rather than element selectors for named slots, they read clearly in the parent's markup and don't force the parent to use a specific tag name.