Key Takeaways
- Angular ng-content projects caller-authored content into placeholders in a reusable component.
- Selector-based slots receive matching nodes; a single unqualified slot catches unmatched content.
- Projection slots are processed at build time. They aren't a mechanism for lazily creating templates.
- Projected content keeps its parent's binding and style ownership. Prefer caller-owned styles and CSS custom properties over ::ng-deep.
Angular ng-content lets a reusable component control its layout while the caller supplies some of its markup. Put <ng-content></ng-content> in the receiving component's template, then place the content between that component's opening and closing tags.
The examples below preserve the simple button-label example and the contact component's heading, phone, and catch-all slots, with complete standalone setup and supported styling patterns.
Understanding Content Projection
A projection slot is an insertion point for content supplied by a component's consumer. Angular describes ng-content as a special element, not a directive that dynamically creates templates.
Keep two kinds of markup separate: content is supplied by the caller; the receiving component's view is its own template. The caller owns bindings in projected content, while the receiver decides where that content appears.
Angular processes ng-content placeholders at build time; no ng-content element remains as a wrapper in the rendered DOM. You can't attach directives or ordinary styling attributes to the placeholder itself.
For the lifecycle side of projected content, see our ngAfterContentInit example and ngAfterContentChecked example.
Set Up the Standalone Angular Example
Use a Node.js release supported by your Angular installation. Follow Angular's installation prerequisites, then create a browser-only project:
npm install -g @angular/cli
ng new projection-demo --standalone --routing=false --style=css --ssr=false
cd projection-demoThe ng new options explicitly select standalone components, disable routing and server-side rendering, and use CSS. Accept any remaining setup prompts.
Create the component files below under src/app. Current Angular components are standalone by default; the examples explicitly include standalone: true to make that setup clear. The consuming component makes dependencies available through its component imports array.
Basic ng-content Example: A Reusable Button
src/app/my-button.ts
import { Component } from '@angular/core';
@Component({
selector: 'my-button',
standalone: true,
template: `
<button type="button" class="action-btn" (click)="action()">
<ng-content></ng-content>
</button>
`,
})
export class MyButtonComponent {
action(): void {
console.log('action triggered');
}
}The receiving component owns the native button and its click handler. The caller supplies its label:
<!-- Consumer template fragment; import MyButtonComponent. -->
<my-button>Click Me!</my-button>
<my-button>Press Here!</my-button>Each label appears inside its component's native button. The button's action() method logs action triggered when its native button is clicked.
This isn't string substitution: the caller can supply markup, such as a span around part of the label, without changing the button component.
Multiple ng-content Slots: The Contact Example
src/app/contact.ts
import { Component } from '@angular/core';
@Component({
selector: 'contact',
standalone: true,
template: `
<div class="contact">
<header>
<ng-content select="h1"></ng-content>
</header>
<div class="contact-phone">
<ng-content select=".phone"></ng-content>
</div>
<div class="contact-details">
<ng-content></ng-content>
</div>
</div>
`,
styles: `
:host { display: block; }
.contact {
color: var(--contact-text-color, #222);
border: 1px solid currentColor;
padding: 1rem;
margin-block: 1rem;
}
`,
})
export class ContactComponent {}The receiver supplies the wrappers. The caller supplies the heading, phone number, and description.
src/app/app.ts
Replace the generated root component with this file. Both reusable components are imported into the consuming component:
import { Component } from '@angular/core';
import { ContactComponent } from './contact';
import { MyButtonComponent } from './my-button';
@Component({
selector: 'app-root',
standalone: true,
imports: [MyButtonComponent, ContactComponent],
template: `
<my-button>Click Me!</my-button>
<my-button>Press Here!</my-button>
<contact>
<h1>John</h1>
<span class="phone">555-433-3322</span>
<p>John is a coworker.</p>
</contact>
<contact class="friend">
<h1>Jane</h1>
<span class="phone">555-334-1123</span>
<p>Jane is a friend.</p>
</contact>
`,
styles: `
.phone { color: #b91c1c; }
.friend { --contact-text-color: #1d4ed8; }
`,
})
export class AppComponent {}src/main.ts
Keep the generated app.config.ts and bootstrap the replacement root component. bootstrapApplication renders a standalone root component and accepts application configuration as its second argument.
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app';
import { appConfig } from './app/app.config';
bootstrapApplication(AppComponent, appConfig).catch((error: unknown) => {
console.error('Application bootstrap failed:', error);
});Keep the generated <app-root></app-root> host in src/index.html. After saving the files, start the application:
ng serveHow Selector Matching and Slot Order Work
Projection assigns each supplied node to a slot; it doesn't copy that node into every matching placeholder. Angular's projection implementation makes the ordering rule explicit:
- The first matching selector-based slot receives a node.
- A node that matches no selected slot goes to the unqualified slot, if one exists.
- Without an unqualified slot, unmatched content isn't rendered into the receiving component's DOM.
For example, <h1 class="phone"> matches both selectors in this contact component. It goes to the heading slot because that selected slot comes first.
Caller-authored content ContactComponent slot
h1 --> select="h1"
span.phone --> select=".phone"
p --> unqualified catch-allUse one catch-all slot and put it last for readability. Its unmatched-content behavior doesn't depend on its physical position.
Slot selectors use Angular's supported selector subset: element names, classes, attributes, combinations of these, and :not(). They aren't arbitrary browser CSS selectors; descendant and child combinators aren't supported.
Matching uses template selector information, not a live DOM query. Changing classes or attributes at runtime doesn't redistribute an existing node between slots. A wrapper also matters: wrapping the heading in a div projects that div as a unit rather than extracting its nested heading.
Attribute-Based Slots and Fallback Content
Selecting h1 is useful for learning, but it couples your component API to a particular heading level. An explicit attribute makes the slot's purpose clearer.
For an attribute-based variant, replace the contact component's heading placeholder with this template fragment:
<!-- ContactComponent template fragment: replaces the h1 slot. -->
<ng-content select="[contactTitle]">
<h2>Unnamed contact</h2>
</ng-content>Then mark the caller's heading:
<!-- Consumer template fragment for the attribute-based variant. -->
<contact>
<h2 contactTitle>John</h2>
<p>John is a coworker.</p>
</contact>Fallback content is written inside the ng-content element and is displayed when that slot has no matching supplied content. Here, omitting the marked heading displays Unnamed contact.
Projecting Components, Directives, and Aliased Nodes
Projected content can include Angular components and elements with directives, not just plain HTML. Import those components and directives into the component that declares the projected markup.
<!-- Consumer template fragment using the original h1/.phone slots. -->
<contact>
<h1>John</h1>
<my-button class="phone">Call John</my-button>
</contact>The my-button host matches the phone slot through its class. Projection doesn't inspect the button component's internal template to find a match.
You can also use ngProjectAs to give a node a different identity for slot matching:
<!-- Consumer template fragment using the original select="h1" slot. -->
<contact>
<h2 ngProjectAs="h1">John</h2>
</contact>This h2 matches the h1 projection slot without becoming an h1 element. The ngProjectAs value must be static; it can't be bound to a dynamic expression.
Styling Projected Content Without ::ng-deep
With Angular's default emulated encapsulation, a component's scoped selectors target elements declared in its own template. That explains why the app component's .phone rule styles the projected spans, while adding the same ordinary scoped rule to ContactComponent doesn't target those spans.
The receiver can style its own wrappers, such as .contact-phone. Normal CSS inheritance still applies, so inherited properties such as color can flow from those wrappers into projected content.
The runnable example demonstrates two supported approaches:
- Caller-owned styling: AppComponent declares the phone spans and its
.phonerule makes them red. - A CSS custom-property API: ContactComponent reads
--contact-text-coloron its own wrapper. The caller sets that property on Jane's host through.friend, making her inherited text blue while the explicit phone color stays red.
CSS custom properties inherit through the DOM, making them useful for documented theme controls. For application-wide conventions, a deliberately global, namespaced class is another option; treat it as a shared styling contract.
When to Use ng-content, Inputs, or ngTemplateOutlet
| Requirement | Choose | Why |
|---|---|---|
| The caller supplies markup inside a reusable layout. | ng-content | The caller owns the markup; the receiver positions it. |
| The component accepts data or configuration. | An input declared with input() or @Input | The receiving component owns how that data is rendered. |
| A template must be instantiated conditionally, repeatedly, or with supplied context. | TemplateRef and NgTemplateOutlet | A template reference represents a fragment that can be instantiated as an embedded view. |
| The component must notify its consumer about an action. | An output | An explicit event API communicates behavior rather than supplying markup. |
Projection doesn't automatically expose the receiver's fields to caller-authored expressions or connect the receiver's actions to parent handlers. Use inputs for data, outputs for custom events, or an explicit template context.
For comparison, this self-contained standalone component uses NgTemplateOutlet to render a template with a context object:
import { NgTemplateOutlet } from '@angular/common';
import { Component } from '@angular/core';
@Component({
selector: 'template-demo',
standalone: true,
imports: [NgTemplateOutlet],
template: `
<ng-template #greeting let-name="name">
<p>Hello, {{ name }}!</p>
</ng-template>
<ng-container
[ngTemplateOutlet]="greeting"
[ngTemplateOutletContext]="{ name: 'John' }">
</ng-container>
`,
})
export class TemplateDemoComponent {}To try it in the application, save it as src/app/template-demo.ts, import TemplateDemoComponent into AppComponent's imports array, and add <template-demo></template-demo> to the app template. The context object's name property supplies the fragment's local name variable.
Conditional Projection Isn't Lazy Rendering
Don't wrap ng-content in @if expecting Angular to defer creating the supplied content. The projection placeholder isn't a runtime template factory, and hiding its location doesn't make the supplied content lazy.
If the entire component and its content should exist only when needed, conditionally render the host in the caller:
<!-- Parent template fragment; showContact is a parent boolean. -->
@if (showContact) {
<contact>
<h1>John</h1>
<p>John is a coworker.</p>
</contact>
}If the receiver must control when a supplied fragment is instantiated, design a TemplateRef-based API instead. Angular's template-fragment guide explains rendering fragments and passing context.
Common Mistakes
- Depending on overlapping selectors accidentally. Use explicit slot attributes where possible, and test content that matches more than one slot.
- Assuming a slot searches nested descendants. Keep slot markers on the supplied top-level nodes, or deliberately project their wrapper.
- Confusing an invisible node with an empty slot. Fallback depends on whether content is supplied, not whether that content contains visible text.
- Testing only the happy path. Cover omitted optional content, unmatched nodes, custom components, caller bindings, and theme overrides.
FAQ
How do multiple ng-content slots match content?
Use select attributes to define selector-based slots. Each supplied node goes to the first matching selected slot; unmatched content goes to an unqualified slot if one exists, otherwise it isn't rendered into the receiver's DOM.
How do I add fallback content to an Angular projection slot?
Put the default markup between the opening and closing ng-content tags. Angular displays it when that slot has no matching supplied content; an empty matching element still fills the slot.
Can I conditionally render ng-content in Angular?
Don't use @if around ng-content as a lazy-rendering mechanism. Conditionally render the receiving component and its content in the caller, or use a TemplateRef-based API when the receiver must control template instantiation.
How do I style projected content without ::ng-deep?
Style projected nodes from the component that declares them, use a documented global class, or expose CSS custom properties through the receiving component's wrappers. With emulated encapsulation, the receiver's ordinary scoped selectors don't directly target caller-declared nodes.
When should I use ng-content instead of ngTemplateOutlet?
Use ng-content when the caller supplies markup and the receiver only needs to position it. Use TemplateRef and NgTemplateOutlet when you need to instantiate a fragment at runtime or supply values through a template context.
Further Reading
- Angular content projection guide — placeholders, multiple slots, fallback content, and ngProjectAs.
- ng-content API reference — the element and supported selector forms.
- Angular CLI ng new reference — project creation options used in this example.
- bootstrapApplication reference — standalone application startup.
- Angular selector guide — supported selectors and static matching.
- Angular component styling guide — encapsulation, global styles, and legacy ::ng-deep behavior.
- Angular template fragments guide — TemplateRef, outlets, and rendering context.
- Angular projection implementation — first-match selection and wildcard routing.
