AngularJSContent Projection (ng-content)

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.

TS
// 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 {}

HTML
<!-- 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.

TS
// 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 {}

HTML
<!-- 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>
Note
The \`select\` attribute accepts any valid CSS selector: element names (\`select="h2"\`), attributes (\`select="[slot-name]"\`), classes (\`select=".header"\`), or combinations.
Fallback Content

Angular 17+ supports fallback content inside <ng-content>. If the parent provides no matching content, the fallback is rendered instead.

TS
@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 {}

HTML
<!-- 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.

HTML
<!-- 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.

TS
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);
  }
}
Tip
Use \`@ContentChildren\` (plural) with a \`QueryList\` to get all matching projected components — e.g. every \`TabComponent\` inside \`TabsComponent\`.
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

TS
// 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();
  }
}

HTML
<!-- 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.