Suggest an editImprove this articleRefine the answer for “Content projection (ng-content) in Angular”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**Content projection (ng-content)** lets a parent component pass HTML into named slots defined inside a child component's template. ```typescript @Component({ selector: 'app-card', template: ` <header><ng-content select="[card-header]"></ng-content></header> <main><ng-content select="[card-body]"></ng-content></main> ` }) export class CardComponent {} ``` **Key point:** projected nodes stay live - their event handlers fire and component lifecycles continue normally. Use `select` for multi-slot projection and `@ContentChildren` to query projected components in `ngAfterContentInit`.Shown above the full answer for quick recall.Answer (EN)Image**Content projection (ng-content)** lets a parent component pass arbitrary HTML into specific slots defined inside a child component's template. ## Theory ### TL;DR - Think of the child component as a magazine template with labeled cutouts. The parent drops content into those cutouts without touching the layout itself. - `ng-content` projects raw DOM nodes from the parent, keeping event handlers, animations, and component lifecycles intact. - Use `select` with CSS selectors to route content into named slots: `[attribute]`, `.class`, or tag name. - Pick it for UI wrappers like cards, modals, and panels. For pure data, use `@Input()` instead. ### Quick example ```typescript // card.component.ts @Component({ selector: 'app-card', standalone: true, template: ` <div class="card"> <header><ng-content select="[card-header]"></ng-content></header> <main><ng-content select="[card-body]"></ng-content></main> <footer><ng-content select="[card-footer]"></ng-content></footer> </div> ` }) export class CardComponent {} ``` ```html <!-- parent usage --> <app-card> <h2 card-header>Profile</h2> <p card-body>Name: Alex</p> <button card-footer (click)="editProfile()">Edit</button> </app-card> ``` Angular matches each element to its slot by the `select` CSS selector and renders the card with the title in the header, the paragraph in the body, and the button in the footer. The button's click handler lives in the parent and fires normally after projection. ### Key difference from @Input() `ng-content` moves actual DOM nodes from the parent into the child's layout. Those nodes stay live: their event handlers fire, their animations run, their child components keep their lifecycle. `@Input()` passes data values, and the child builds its own markup from them. When you need markup from the parent inside the child's structure, not just a string or an object, `ng-content` is the right call. ### When to use - **Reusable wrappers** (cards, panels, modals, drawers): define named slots with `select` so each layout zone has a clear owner. - **Dynamic list items**: a single `<ng-content>` without `select` lets the parent fully control the item template. - **Third-party component embedding**: projection preserves the embedded component's `ngOnInit`/`ngOnDestroy`, so its internal state stays clean. - **Data-only situations**: use `@Input()`. No DOM overhead, full type safety, and easier to test. ### How Angular compiles it During template compilation, Angular scans `<ng-content>` tags and records each `select` value as a CSS selector. At runtime, it matches nodes from the parent against those selectors and inserts the matching DOM fragments directly into the child's view, without cloning or re-rendering them. Nodes that do not match any `select` fall through to a default `<ng-content>` (one with no `select` attribute), if the child defines one. ### Select syntax reference ```html <!-- Match by attribute (most common) --> <ng-content select="[card-header]"></ng-content> <!-- Match by CSS class --> <ng-content select=".card-footer"></ng-content> <!-- Match by tag name --> <ng-content select="h2"></ng-content> <!-- Match by component selector --> <ng-content select="app-icon"></ng-content> <!-- Default: catches everything not matched above --> <ng-content></ng-content> ``` ### Common mistakes **1. Using a bare tag selector when the element carries a named attribute** ```html <!-- Wrong: does not match <h2 card-header>My Title</h2> --> <ng-content select="h2"></ng-content> <!-- Fix: match by the attribute --> <ng-content select="[card-header]"></ng-content> <!-- Or combine tag + attribute for precision --> <ng-content select="h2[card-header]"></ng-content> ``` The `select` value is a CSS selector. `h2` matches any `<h2>` tag. `[card-header]` matches any element carrying that attribute. They are not the same. **2. Multiple `<ng-content>` tags without `select`** ```html <!-- Wrong: all content goes into the first slot, the second is always empty --> <header><ng-content></ng-content></header> <main><ng-content></ng-content></main> <!-- Fix: each slot needs its own selector --> <header><ng-content select="[card-header]"></ng-content></header> <main><ng-content select="[card-body]"></ng-content></main> ``` **3. Projecting content controlled by `*ngIf` that starts as false** ```html <!-- Unreliable: if show is false on load, the node does not exist yet and there is nothing to project --> <app-card><p *ngIf="show">Conditional text</p></app-card> <!-- Fix: wrap in ng-container so Angular tracks the slot correctly --> <app-card> <ng-container *ngIf="show"><p>Conditional text</p></ng-container> </app-card> ``` **4. Expecting child component styles to reach projected nodes** Angular's `ViewEncapsulation.Emulated` (the default) adds attribute selectors to the child's styles. Those attribute selectors do not apply to projected nodes because the nodes belong to the parent's view. To style projected content from the child, either move the styles to the parent or set `encapsulation: ViewEncapsulation.None` on the child and use targeted selectors carefully. ### Accessing projected content in TypeScript Use `@ContentChild` and `@ContentChildren` to query projected components after they land in the view: ```typescript import { Component, ContentChildren, QueryList, AfterContentInit } from '@angular/core'; import { TabComponent } from './tab.component'; @Component({ selector: 'app-tabs', standalone: true, template: `<ng-content></ng-content>` }) export class TabsComponent implements AfterContentInit { @ContentChildren(TabComponent) tabs!: QueryList<TabComponent>; ngAfterContentInit() { // projected content is available here, not in ngOnInit if (!this.tabs.some(tab => tab.active)) { this.tabs.first.active = true; } } } ``` Query projected components in `ngAfterContentInit`. Reading `@ContentChildren` in `ngOnInit` returns an empty list because projection has not happened yet. ### Real-world usage - **Angular Material:** `mat-card` slots header, subtitle, image, and actions through named `ng-content` selectors like `[mat-card-title]` and `[mat-card-content]`. - **NG Bootstrap:** `ngb-modal` projects a custom header and footer into named slots. - **PrimeNG:** `p-table` combines `ng-content` with `ng-template` and `pTemplate` directives for column and row definitions. - **Custom design systems:** most teams build card, modal, and drawer wrappers this way so product developers control markup without touching the component's internal layout. ### Follow-up questions **Q:** What is the difference between `<ng-content>` and `*ngTemplateOutlet`? **A:** `ng-content` projects the parent's existing DOM nodes into a slot inside the child. `*ngTemplateOutlet` renders a `TemplateRef` and can pass a context object into it. Use `ng-content` when the parent owns the markup. Use `ngTemplateOutlet` when the child needs to render a template multiple times or inject data into it. **Q:** Can projected content pass through multiple component levels? **A:** Yes. Each intermediate component needs its own `<ng-content>` to forward content deeper. Angular tunnels the nodes through each level down to the first matching slot. **Q:** Does content projection affect performance compared to `@Input()`? **A:** Projection involves more DOM work than data binding, so it is measurably slower for large lists with many projected nodes. Adding `OnPush` change detection to the child component reduces unnecessary re-renders and offsets most of the cost. **Q:** How does `ViewEncapsulation` affect styling projected content? **A:** `Emulated` (default) scopes child styles with attribute selectors that do not reach projected nodes. `ShadowDom` fully isolates the child's view. `None` disables encapsulation entirely. For components that need to style their projected slots from the child side, `None` plus specific selectors is the standard approach. **Q:** (Senior) Which lifecycle hook fires when projected content is ready, and why does it matter? **A:** `ngAfterContentInit` fires after Angular finishes projecting content into the view. `ngOnInit` fires before projection, so any `@ContentChild` or `@ContentChildren` query there returns nothing. `ngAfterContentChecked` fires after each change detection pass that touches projected content. ## Examples ### Basic: single-slot card ```typescript @Component({ selector: 'app-card', standalone: true, template: ` <div class="card"> <ng-content></ng-content> </div> ` }) export class CardComponent {} ``` ```html <app-card> <h2>User Profile</h2> <p>Name: Alex</p> <button (click)="editProfile()">Edit</button> </app-card> ``` The click handler lives in the parent and keeps firing after the button is projected. Passing `name` as an `@Input()` and rendering the button inside the child would require the child to also emit a `(click)` event, which is more wiring for less flexibility. ### Intermediate: modal with named slots ```typescript // modal.component.ts @Component({ selector: 'app-modal', standalone: true, imports: [NgIf], template: ` <div class="modal-overlay" *ngIf="open"> <div class="modal"> <ng-content select="[modal-title]"></ng-content> <ng-content select="[modal-content]"></ng-content> <ng-content select="[modal-actions]"></ng-content> </div> </div> ` }) export class ModalComponent { @Input() open = false; } ``` ```html <app-modal [open]="isOpen"> <h1 modal-title>Confirm Delete</h1> <p modal-content>This action cannot be undone.</p> <div modal-actions> <button (click)="confirmDelete()">Yes, delete</button> <button (click)="isOpen = false">Cancel</button> </div> </app-modal> ``` The modal owns layout and visibility. The parent owns text and logic. Testing the delete flow does not require mounting the modal internals at all. ### Advanced: tabs with @ContentChildren ```typescript // tabs.component.ts @Component({ selector: 'app-tabs', standalone: true, template: ` <div class="tab-bar"> <button *ngFor="let tab of tabs" (click)="activate(tab)" [class.active]="tab.active"> {{ tab.title }} </button> </div> <ng-content></ng-content> ` }) export class TabsComponent implements AfterContentInit { @ContentChildren(TabComponent) tabs!: QueryList<TabComponent>; ngAfterContentInit() { if (!this.tabs.some(t => t.active)) { this.tabs.first.active = true; } } activate(selected: TabComponent) { this.tabs.forEach(tab => (tab.active = false)); selected.active = true; } } ``` ```html <app-tabs> <app-tab title="Overview">Overview content</app-tab> <app-tab title="Settings">Settings content</app-tab> <app-tab title="Billing" [active]="true">Billing content</app-tab> </app-tabs> ``` `QueryList` updates automatically when tabs are added or removed dynamically. That is what makes this pattern work without any extra event handling on the parent side.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.