Skip to content
Angular ng components 4 min read

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 typeExample select valueMatches
Elementselect="header"<header>...</header>
Attributeselect="[card-body]"<div card-body>...</div>
Classselect=".actions"<div class="actions">...</div>
Combinedselect="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.
Last updated June 14, 2026
Was this helpful?